TypeScript의 타입 시스템은 값을 다루지 않지만, 타입 단계에서도 if문처럼 조건에 따라 다른 타입을 선택할 수 있습니다. 이것이 조건부 타입(Conditional Types)입니다.
T extends U ? X : Y
T가 U에 할당 가능하면 X, 아니면 Y를 반환합니다. 조건부 타입에서 extends는 상속이 아닌 타입 호환성 검사입니다.
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true — "hello"는 string에 할당 가능
Exclude, Extract, NonNullable 같은 유틸리티 타입이 조건부 타입으로 구현되어 있습니다.
// Exclude: U에 해당하는 타입 제거
type MyExclude<T, U> = T extends U ? never : T;
type Result = MyExclude<"a" | "b" | "c", "b">;
// "a" | "c"
// NonNullable: null과 undefined 제거
type MyNonNullable<T> = T extends null | undefined ? never : T;
type Clean = MyNonNullable<string | null | undefined>;
// string
타입 매개변수가 유니온 타입이면, 조건부 타입이 각 멤버에 분배 적용됩니다.
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] (분배 적용)
// — string에 적용 → string[]
// — number에 적용 → number[]
분배 법칙을 막으려면 타입을 튜플로 감쌉니다.
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNoDistribute<string | number>;
// (string | number)[] (분배 안 됨)
분배 법칙(Distributive Conditional Types): 벗겨진(naked) 타입 매개변수에 조건부 타입을 적용하면 유니온의 각 멤버에 개별 적용됩니다.
조건부 타입 안에서 타입의 일부를 변수처럼 캡처하는 키워드입니다.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: "철수" };
}
type UserReturn = ReturnType<typeof getUser>;
// { id: number; name: string }
infer R은 "함수의 반환 타입을 R로 캡처해 사용하겠다"는 의미입니다.
// 함수 매개변수 타입 추출
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
function greet(name: string, age: number) {}
type FirstArg = FirstParam<typeof greet>; // string
// Promise가 감싼 타입 추출
type Awaited<T> = T extends Promise<infer V> ? V : T;
type Resolved = Awaited<Promise<string>>; // string
type Direct = Awaited<number>; // number
// 배열 원소 타입 추출
type ElementType<T> = T extends (infer E)[] ? E : never;
type Str = ElementType<string[]>; // string
type Num = ElementType<number[]>; // number
자기 자신을 참조하는 재귀 구조도 만들 수 있습니다.
// 중첩 배열을 펼쳐서 원소 타입 추출
type FlatArray<T> = T extends (infer U)[]
? U extends any[]
? FlatArray<U>
: U
: T;
type Flat = FlatArray<number[][][]>; // number
// 입력 타입에 따라 반환 타입이 달라지는 함수
type WrapResult<T> = T extends string
? { type: "string"; value: T }
: T extends number
? { type: "number"; value: T }
: { type: "other"; value: T };
function wrap<T>(value: T): WrapResult<T> {
if (typeof value === "string") {
return { type: "string", value } as WrapResult<T>;
}
if (typeof value === "number") {
return { type: "number", value } as WrapResult<T>;
}
return { type: "other", value } as WrapResult<T>;
}
const s = wrap("hello"); // { type: "string"; value: string }
const n = wrap(42); // { type: "number"; value: number }
조건부 타입은 TypeScript 타입 시스템에서 가장 표현력이 높은 기능입니다. infer와 결합하면 함수 시그니처, Promise, 배열 등 복잡한 타입 구조에서 원하는 부분만 정밀하게 추출할 수 있습니다.