조건부 타입 — T extends U ? X : Y
타입 안에 if 가 있는 듯한 강력한 도구. infer 와 분배 조건부까지.
유틸리티 타입(11편) 들이 어떻게 만들어졌나 들여다보면 거의 모두 조건부 타입이 기반입니다. 자바스크립트의 삼항 연산자(x ? a : b) 와 같은 모양 — 다만 타입 레벨에서 동작합니다. 12편은 조건부 타입과 그 동반자 infer·분배 조건부 타입까지.
기본형 — T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
type C = IsString<string>; // true
// 실용 예 — null/undefined 제거 (NonNullable 의 정의)
type MyNonNullable<T> = T extends null | undefined ? never : T;
읽는 방법. "T 가 U 의 부분집합(할당 가능) 이면 X, 아니면 Y". extends 는 "상속" 보다는 "이것에 할당 가능" 으로 읽는 게 정확.
infer — 매치된 일부를 변수로 캡처
// 함수의 반환 타입만 뽑기 (ReturnType 의 정의)
type MyReturnType<F> = F extends (...args: any) => infer R ? R : never;
type R1 = MyReturnType<() => number>; // number
type R2 = MyReturnType<(x: string) => User>; // User
// Promise 안 풀기 (Awaited 의 핵심)
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type V = Unwrap<Promise<number>>; // number
// 배열의 원소 타입
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<User[]>; // User
infer 는 "타입 매칭하면서 일부를 변수에 담아두기". 정규식의 캡처 그룹과 닮았습니다.
분배 조건부 타입 — 유니온이 자동 분기
type ToArray<T> = T extends any ? T[] : never;
type X = ToArray<string | number>;
// 의외: string[] | number[] ← 유니온 각 멤버에 자동 분배
// 직관: (string | number)[] ← 아님!
// 왜? 유니온 + 조건부 = 자동 분배 (벗기지 않게 하려면 [] 로 감싸기)
type ToArrayNo<T> = [T] extends [any] ? T[] : never;
type Y = ToArrayNo<string | number>; // (string | number)[]
분배의 함정. "T extends U" 에서 T 가 유니온이면 자동으로 멤버별로 풀려서 적용됩니다. 대부분 유용하지만 가끔 의도와 다를 때 위 [T] extends [U] 트릭으로 막습니다.
Exclude · Extract — 분배의 응용
// 유틸리티 타입의 실제 정의 (단순)
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
// 동작 (분배 덕분)
type A = MyExclude<"a" | "b" | "c", "b">;
// = ("a" extends "b" ? never : "a") → "a"
// | ("b" extends "b" ? never : "b") → never
// | ("c" extends "b" ? never : "c") → "c"
// = "a" | "c"
실전 — API 응답 타입 안전하게 풀기
type ApiResponse<T> =
| { ok: true; data: T }
| { ok: false; error: string };
// 성공만 골라 data 타입 뽑기
type SuccessData<R> = R extends { ok: true; data: infer D } ? D : never;
type UserResp = ApiResponse<{ id: number; name: string }>;
type UserData = SuccessData<UserResp>;
// = { id: number; name: string }
literal 좁히기 + 조건부
// 입력 타입에 따라 반환 타입이 달라지는 함수
function format<T extends number | string>(x: T): T extends number ? string : number {
return (typeof x === "number" ? String(x) : Number(x)) as any;
}
const a = format(10); // string
const b = format("10"); // number
이게 overload(3편) 의 더 강력한 대안. 입력-출력 관계를 한 줄로 표현.
실전 자주 만나는 조건부 타입 패턴
| 패턴 | 역할 |
|---|---|
| T extends Function ? ... : ... | 함수만 분기 |
| T extends any[] ? ... : ... | 배열만 분기 |
| T extends Promise<infer V> ? V : T | Promise 풀기 |
| F extends (...a: infer A) => any ? A : never | 함수 매개변수 튜플 |
| T extends { type: U } ? T : never | discriminated 유니온 좁히기 |
| T extends \`${U}${infer R}\` ? R : never | 템플릿 리터럴 타입 (5.0+) |
너무 깊게 들어가지 마세요. 조건부 타입을 5-6 단계 중첩한 코드는 누구도 못 읽습니다. 라이브러리 코드라면 가치 있지만, 일반 앱 코드는 유틸리티 타입 조합 으로 끝내는 게 보통 정답.
13편 — 매핑 타입 (keyof + [K in T])
속성 단위로 타입을 변형하는 더 강력한 도구.
📚 쉽게 배우는 타입스크립트 교재
이전: 11편 유틸리티 · 현재: 12편 (중급) · 다음 → 13편 매핑 타입 · 진행: 12/20
이전: 11편 유틸리티 · 현재: 12편 (중급) · 다음 → 13편 매핑 타입 · 진행: 12/20