TypeScript Conditional Type과 infer 조건에 따라 타입 만들기

··설명 약 8분·작성: 해비·블로그 소개

TypeScript Conditional Type과 infer는 타입을 어떻게 선택하고 추출하나요?

Conditional Type은 값이 아닌 타입의 관계를 조건으로 삼아 결과 타입을 고릅니다. T extends U ? X : Y로 분기하고, 참인 분기의 infer로 Promise나 배열 안쪽 타입을 꺼낼 수 있습니다.

적용 조건·범위: 분배는 단순히 union이 있다는 이유만으로 항상 일어나지 않습니다. 조건 왼쪽에 그대로 놓인 제네릭 매개변수 T가 union을 받을 때 각 구성원을 따로 검사하며, [T]처럼 감싸면 union 전체를 한 번에 비교합니다. 실제 Promise를 기다려 값을 얻는 일은 await가 담당합니다.

본문 근거: TypeScript Conditional Types·infer·분배

직접 확인: 오류에서 수정까지

TypeScript Conditional Type과 infer 조건에 따라 타입 만들기 · 독립 실습

얻을 결과: infer로 Promise 내부 값을 추론하고 실제 변수 타입에 반영합니다.

예제 크게 보기 (새 탭)

단독 검사는 tsconfig.json이 없는 새 폴더에서 example.ts 한 파일씩 교체하며 진행합니다. TypeScript가 없다면 설치 글에서 Node.js/npm·TypeScript 준비를 확인하세요. 타입 검사 성공은 코드 실행 성공과 구분합니다.

독자 질문: Promise 응답에서 내부 데이터 타입만 추출하려면?

얻을 결과: infer로 Promise 내부 값을 추론하고 실제 변수 타입에 반영합니다.

다음 코드를 별도 example.ts에 복사합니다.

type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Data = Unwrap<Promise<number>>;
const value: Data = '1';

예상 오류 (TS2322): 추출된 Data는 number이므로 문자열 ‘1’을 할당할 수 없습니다. 숫자 값을 사용합니다.

오류 예제를 다음 코드로 교체합니다.

type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Data = Unwrap<Promise<number>>;
const value: Data = 1;
console.log(value);
npx tsc --strict --noEmit example.ts

기존 최소 예제 확인 기록: TypeScript 7.0.2의 별도 최소 예제에서 오류 TS2322, 수정 후 exit 0 확인. TaskFlow 전체 체크포인트는 미확인.

다음 학습: 15회차에서 판별 유니온과 never로 상태 누락을 찾습니다.

기본 예제

아래 개념 조각에서 참조하는 학습용 타입과 값입니다. 프로젝트 파일을 import하지 않습니다. 같은 이름을 다시 선언하는 비교 코드는 별도 파일에서 확인하세요.

type Task = { id: string; title: string; completed: boolean };
function createTask(): Task { return { id: '1', title: '학습', completed: false }; }
type IsString<T> =
  T extends string
    ? true
    : false;


type A =
  IsString<string>;


type B =
  IsString<number>;

A는 true, B는 false 타입입니다.

property 존재 여부에 따른 타입

type MessageOf<T> =
  T extends {
    message: unknown;
  }
    ? T['message']
    : never;

message property가 있는 타입이면 해당 property 타입을 반환합니다.

infer

조건부 타입 안에서 일부 타입을 추론해 이름을 붙일 수 있습니다.

Promise 내부 타입:

type UnwrapPromise<T> =
  T extends Promise<infer R>
    ? R
    : T;
type Result =
  UnwrapPromise<
    Promise<Task[]>
  >;

Result는 Task[]입니다.

배열 element 타입

type ElementOf<T> =
  T extends Array<infer U>
    ? U
    : never;
type TaskItem =
  ElementOf<Task[]>;

TaskItem은 Task입니다.

readonly 배열까지 고려하면 조건을 확장할 수 있습니다.

함수 반환 타입

TypeScript에는 ReturnType 같은 built-in utility가 이미 있습니다.

type Result =
  ReturnType<
    typeof createTask
  >;

이미 제공되는 Utility Type이 있다면 직접 복잡한 conditional type을 다시 만들 필요가 없습니다.

분배 조건부 타입

generic T에 union이 들어가면 conditional type이 union 각 구성원에 분배되는 경우가 있습니다.

type ToArray<T> =
  T extends unknown
    ? T[]
    : never;


type Result =
  ToArray<
    string | number
  >;

결과:

string[] | number[]

분배를 원하지 않으면 tuple로 감싸는 패턴을 사용할 수 있습니다.

type ToArrayNonDist<T> =
  [T] extends [unknown]
    ? T[]
    : never;

고급 기능이므로 실제 필요가 있을 때 사용합니다.

Conditional Type을 남용하면

다음 문제가 생길 수 있습니다.

  • 오류 메시지가 어려워짐
  • IDE hover 타입이 매우 길어짐
  • 팀원이 타입 의도를 이해하기 어려움
  • 실제 runtime 구조와 타입 선언이 멀어짐

간단한 union으로 충분하면 union을 사용합니다.

분배는 단순히 union이 있다는 이유만으로 항상 일어나지 않습니다. 조건 왼쪽에 그대로 놓인 제네릭 매개변수 T가 union을 받을 때 각 구성원을 따로 검사하며, [T]처럼 감싸면 union 전체를 한 번에 비교합니다. 실제 Promise를 기다려 값을 얻는 일은 await가 담당합니다.

원리 확인: TypeScript Conditional Types·infer·분배 (2026-10-12). 아래 기대 결과와 기존 실행 기록은 구분해서 읽습니다.

학습 완료 체크리스트

Unwrap의 입력이 Promise<number>, Promise<string>, boolean일 때 결과를 각각 설명해 보세요. infer는 값을 꺼내 실행하는 코드가 아니라 타입을 추출하는 선언이라는 점도 구분합니다.


작은 변형으로 확인하기

Unwrap<Promise<string>>의 Data는 string입니다. value를 문자열로 바꾸고 숫자를 대입했을 때 오류가 나는지 비교하세요.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript 코드를 TypeScript로 확장합니다.

TaskFlow 비동기 helper 타입에서 Promise 결과를 추출하는 원리를 익힙니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow 프로젝트 적용

TypeScript Conditional Type과 infer 조건에 따라 타입 만들기 · 단계별 TaskFlow

타입을 적용한 TaskFlow에서 작업을 추가·완료 전환·삭제하세요. 타입만 바뀐 단계는 이전과 화면이 같습니다.

예제 크게 보기 (새 탭)

시작·완료 파일과 적용 순서에서 이번 변경 파일의 전체 내용과 적용 순서를 확인합니다.

시작본과 이번 변경

TypeScript Mapped Type 객체 타입을 기반으로 새 타입 만들기의 완료 체크포인트를 기준으로 이어갑니다. 변경하지 않는 파일은 그 시작 상태를 유지합니다.

src/types/async.ts에 AsyncResult를 추가하고 사용하는 파일에서 Task와 AsyncResult를 import합니다. fetchTasks의 반환 계약으로 FetchedTasks를 파생해 hover 또는 잘못된 할당으로 확인합니다.

이전 체크포인트

typescript-step-13-mapped-types

이번 목표

TaskFlow 비동기 helper 타입에서 Promise 결과를 추출하는 원리를 익힙니다.

전체 파일·코드 보기

src/types/async.ts

export type AsyncResult<T> =
  T extends Promise<infer R>
    ? R
    : T;

예제

async function fetchTasks():
  Promise<Task[]> {
  return [];
}


type FetchedTasks =
  AsyncResult<
    ReturnType<
      typeof fetchTasks
    >
  >;

FetchedTasks는 Task[]입니다.

실제 프로젝트에서는 Awaited<ReturnType<…>>처럼 built-in utility를 우선할 수 있습니다.

검증

npx tsc --noEmit

중간부터 시작하는 전체 파일과 적용 순서

이번 글 시작 ZIP · 이번 글 완료 ZIP

시작 ZIP은 직전 프로젝트 단계 typescript-step-13-mapped-types의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.

  1. 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
  2. 조건부 타입과 실제 빈 작업 배열 Promise 예제를 별도 파일로 연결합니다.
  3. 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
  4. 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.

추가·교체 파일

  • CHANGES.md — 전체 교체
  • README.md — 전체 교체
  • tests/contracts.ts — 전체 교체
  • src/types/async.ts — 추가
  • src/core/async-example.ts — 추가

실행과 확인

npm ci
npm run dev
npm run typecheck
npm test
npm run build

개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.

예상 결과: Promise<Task[]>에서 Task[]가 추출되며 Promise가 아닌 값의 타입은 유지됩니다.

기존 ZIP 검증 기록: TypeScript strict 검사·긍정/부정 타입 계약·런타임 검사·Vite 빌드 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.

이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.

복사할 변경 파일 전체 코드

tests/contracts.ts — 전체 파일

import type { Task } from '../src/types/task.js';
const valid: Task = {id:'1',title:'공부',completed:false};
// @ts-expect-error completed must be boolean
const invalid: Task = {id:'1',title:'공부',completed:'no'};
void valid; void invalid;
import { readTitle } from '../src/core/selection.js';
const nullable = readTitle(null);
// @ts-expect-error null result must be narrowed
const definite: string = nullable;
void definite;
import type { TaskKey } from '../src/types/task-key.js';
// @ts-expect-error nonexistent property
const badKey: TaskKey = 'missing';
void badKey;
import { TASK_FILTER_LABELS } from '../src/constants/task.js';
// @ts-expect-error missing is not a filter key
const badLabel = TASK_FILTER_LABELS.missing;
void badLabel;
import { getValue } from '../src/core/collection.js';
// @ts-expect-error nonexistent key must fail
getValue({title:'ok'}, 'missing');
import type { TaskFormDirty } from '../src/types/form.js';
// @ts-expect-error completed is required by mapped type
const dirty: TaskFormDirty = {title:true};
void dirty;
import type { AsyncResult } from '../src/types/async.js';
const result: AsyncResult<Promise<Task[]>> = [];
// @ts-expect-error Promise value must resolve to Task array
const wrongResult: AsyncResult<Promise<Task[]>> = 1;
void result;void wrongResult;

src/types/async.ts — 전체 파일

export type AsyncResult<T> =
  T extends Promise<infer R>
    ? R
    : T;

src/core/async-example.ts — 전체 파일

import type { Task } from '../types/task.js';
import type { AsyncResult } from '../types/async.js';
export async function fetchTasks():
  Promise<Task[]> {
  return [];
}


export type FetchedTasks =
  AsyncResult<
    ReturnType<
      typeof fetchTasks
    >
  >;

완료 체크포인트

typescript-step-14-conditional-types

다음 학습

다음 글에서는 판별 유니온으로 loading, success, error 상태를 모델링하고 never로 빠진 상태 처리를 찾습니다.

다음 글: TypeScript 고급 상태 모델링: 판별 유니온과 never 타입 테스트

글 소개·학습 준비·전체 목차
TypeScript Conditional Type과 infer 조건에 따라 타입 만들기

TypeScript · 14 / 22 · 고급 학습 · Conditional Type과 infer

조건에 따라 타입을 고르거나 내부 타입만 꺼내려면 어떻게 할까요? Conditional Type의 extends로 타입 관계를 검사하고 참인 분기의 infer로 일부 타입에 이름을 붙입니다. Promise<number>에서 number를 추출한 뒤 잘못된 값 할당을 검사하는 예제로 결과를 확인합니다.

기본 문법은 JavaScript 삼항 연산자와 비슷합니다.

T extends U
  ? X
  : Y

하지만 실제 값이 아니라 타입 관계를 검사합니다.

이번 글에서 배우는 내용

  • Conditional Type
  • extends 조건
  • generic과 조건부 타입
  • distributive conditional type
  • infer
  • Promise/배열 내부 타입 추출
  • 복잡한 타입을 피하는 기준

단독 학습 선수 지식

제네릭·union·Promise 타입을 알면 됩니다. 이전 TaskFlow 파일 없이 직접 확인 예제로 학습할 수 있습니다.

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 22개 · 필수 21개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기