전체 목록
TypeScriptHard#101

TypeScript의 조건부 타입(Conditional Types)과 infer 키워드를 설명해주세요.

#TS#조건부타입#infer#제네릭

답변 포인트

T extends U ? X : Y 형태와 타입 일부를 추론하는 infer의 역할을 떠올려보세요.

정답 및 해설

빠른 요약

조건부 타입은 타입이 특정 조건을 만족하는지에 따라 다른 타입을 선택하는 기능입니다. 기본 형태는 T extends U ?

조건부 타입은 타입 수준에서 T extends U ? X : Y 형태로 분기하는 기능입니다. 런타임의 if 문처럼 값에 따라 분기하는 것이 아니라, 타입이 특정 조건을 만족하는지에 따라 다른 타입을 선택합니다.

기본 형태

TypeScript
type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

T extends string은 "Tstring에 할당 가능한 타입인가?"를 의미합니다.

실무에서 자주 보는 예

TypeScript
type ApiResponse<T> = T extends Error
  ? { ok: false; error: T }
  : { ok: true; data: T };

type Success = ApiResponse<User>;
type Failure = ApiResponse<Error>;

입력 타입에 따라 응답 타입을 다르게 표현할 수 있습니다.

유니온 타입과 분배 조건부 타입

조건부 타입의 대상이 제네릭 타입 파라미터이고 유니온이 들어오면 각 멤버에 대해 조건이 분배됩니다.

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

type Result = ToArray<string | number>;
// string[] | number[]

분배를 막고 싶다면 튜플로 감쌉니다.

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

type Result = ToArrayNonDistributed<string | number>;
// (string | number)[]

infer 키워드

infer는 조건부 타입 안에서 타입의 일부를 추론해 이름 붙일 때 사용합니다.

TypeScript
type ReturnTypeOf<T> = T extends (...args: unknown[]) => infer R ? R : never;

type A = ReturnTypeOf<() => string>; // string
type B = ReturnTypeOf<(id: number) => Promise<User>>; // Promise<User>

배열의 요소 타입을 추출할 때도 사용할 수 있습니다.

TypeScript
type ElementType<T> = T extends (infer U)[] ? U : T;

type A = ElementType<string[]>; // string
type B = ElementType<number>;   // number

Promise 내부 타입 추출

TypeScript
type AwaitedValue<T> = T extends Promise<infer U> ? U : T;

type UserResult = AwaitedValue<Promise<User>>; // User
type CountResult = AwaitedValue<number>;       // number

중첩된 Promise까지 풀고 싶다면 재귀 조건부 타입을 사용할 수 있습니다.

TypeScript
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;

type Result = DeepAwaited<Promise<Promise<string>>>; // string

주의할 점

조건부 타입은 강력하지만 너무 복잡하게 사용하면 타입을 읽기 어려워집니다. 특히 재귀 타입이나 과도한 infer 조합은 컴파일 성능과 가독성에 영향을 줄 수 있습니다.

정리

개념설명
조건부 타입타입이 특정 조건을 만족하는지에 따라 다른 타입 선택
분배 조건부 타입유니온 타입의 각 멤버에 조건이 적용됨
infer조건부 타입 안에서 타입 일부를 추론
활용ReturnType, Parameters, Awaited 같은 유틸리티 타입 구현

실무 감각으로 이해하기

조건부 타입은 “값을 검사하는 코드”가 아니라 타입 선언을 입력받아 새 타입을 계산하는 함수에 가깝습니다. API 클라이언트, 폼 스키마, 이벤트 맵처럼 입력 타입에 따라 결과 타입이 달라져야 할 때 유용합니다.

TypeScript
type UnwrapResponse<T> = T extends { data: infer D } ? D : T;

type User = UnwrapResponse<{ data: { id: string; name: string } }>;
// { id: string; name: string }

자주 쓰는 infer 패턴

TypeScript
type FirstArg<T> = T extends (arg: infer A, ...rest: unknown[]) => unknown ? A : never;
type ArrayItem<T> = T extends readonly (infer Item)[] ? Item : never;
type PromiseValue<T> = T extends Promise<infer V> ? V : T;

type A = FirstArg<(id: number) => void>; // number
type B = ArrayItem<readonly ['admin', 'user']>; // 'admin' | 'user'

infer는 아무 곳에서나 사용할 수 없고 extends의 조건부 타입 내부에서만 사용할 수 있습니다. 또한 추론한 타입을 실제 런타임 값처럼 사용할 수는 없습니다.

실무 팁

  • 공개 API 타입은 너무 영리하게 만들기보다 읽기 쉬운 이름을 붙입니다.
  • 유니온 분배가 의도인지 먼저 확인합니다. 의도하지 않았다면 [T] extends [U] 형태로 감쌉니다.
  • 재귀 조건부 타입은 편하지만 타입 검사 시간이 늘 수 있으므로 깊이를 제한하거나 단순화합니다.

관련 질문

같은 카테고리/태그 기준