TypeScript의 조건부 타입(Conditional Types)과 infer 키워드를 설명해주세요.
답변 포인트
T extends U ? X : Y 형태와 타입 일부를 추론하는 infer의 역할을 떠올려보세요.
정답 및 해설
빠른 요약
조건부 타입은 타입이 특정 조건을 만족하는지에 따라 다른 타입을 선택하는 기능입니다. 기본 형태는 T extends U ?
조건부 타입은 타입 수준에서 T extends U ? X : Y 형태로 분기하는 기능입니다. 런타임의 if 문처럼 값에 따라 분기하는 것이 아니라, 타입이 특정 조건을 만족하는지에 따라 다른 타입을 선택합니다.
기본 형태
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // falseT extends string은 "T가 string에 할당 가능한 타입인가?"를 의미합니다.
실무에서 자주 보는 예
type ApiResponse<T> = T extends Error
? { ok: false; error: T }
: { ok: true; data: T };
type Success = ApiResponse<User>;
type Failure = ApiResponse<Error>;입력 타입에 따라 응답 타입을 다르게 표현할 수 있습니다.
유니온 타입과 분배 조건부 타입
조건부 타입의 대상이 제네릭 타입 파라미터이고 유니온이 들어오면 각 멤버에 대해 조건이 분배됩니다.
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[]분배를 막고 싶다면 튜플로 감쌉니다.
type ToArrayNonDistributed<T> = [T] extends [unknown] ? T[] : never;
type Result = ToArrayNonDistributed<string | number>;
// (string | number)[]infer 키워드
infer는 조건부 타입 안에서 타입의 일부를 추론해 이름 붙일 때 사용합니다.
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>배열의 요소 타입을 추출할 때도 사용할 수 있습니다.
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string
type B = ElementType<number>; // numberPromise 내부 타입 추출
type AwaitedValue<T> = T extends Promise<infer U> ? U : T;
type UserResult = AwaitedValue<Promise<User>>; // User
type CountResult = AwaitedValue<number>; // number중첩된 Promise까지 풀고 싶다면 재귀 조건부 타입을 사용할 수 있습니다.
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type Result = DeepAwaited<Promise<Promise<string>>>; // string주의할 점
조건부 타입은 강력하지만 너무 복잡하게 사용하면 타입을 읽기 어려워집니다. 특히 재귀 타입이나 과도한 infer 조합은 컴파일 성능과 가독성에 영향을 줄 수 있습니다.
정리
| 개념 | 설명 |
|---|---|
| 조건부 타입 | 타입이 특정 조건을 만족하는지에 따라 다른 타입 선택 |
| 분배 조건부 타입 | 유니온 타입의 각 멤버에 조건이 적용됨 |
| infer | 조건부 타입 안에서 타입 일부를 추론 |
| 활용 | ReturnType, Parameters, Awaited 같은 유틸리티 타입 구현 |
실무 감각으로 이해하기
조건부 타입은 “값을 검사하는 코드”가 아니라 타입 선언을 입력받아 새 타입을 계산하는 함수에 가깝습니다. API 클라이언트, 폼 스키마, 이벤트 맵처럼 입력 타입에 따라 결과 타입이 달라져야 할 때 유용합니다.
type UnwrapResponse<T> = T extends { data: infer D } ? D : T;
type User = UnwrapResponse<{ data: { id: string; name: string } }>;
// { id: string; name: string }자주 쓰는 infer 패턴
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]형태로 감쌉니다. - 재귀 조건부 타입은 편하지만 타입 검사 시간이 늘 수 있으므로 깊이를 제한하거나 단순화합니다.