조건부 타입은 JavaScript의 삼항 연산자처럼 읽히는 검사로 두 타입 중 하나를 고릅니다: T extends U ? X : Y. T가 U에 대입 가능하면 결과는 X, 그렇지 않으면 Y입니다.
여기서 extends는 "대입 가능하다"는 뜻으로, 변수에 값을 대입할 때 컴파일러가 쓰는 것과 같은 관계입니다. 조건부 타입은 컴파일 시점에만 존재하며 JavaScript 출력에서 지워집니다.
문법
type Result = CheckedType extends TestType ? TrueType : FalseType;
조건부 타입은 검사하는 타입이 나중에 구체적인 타입을 받는 타입 매개변수인 제네릭과 함께일 때 쓸모가 있습니다. true 분기에서 TypeScript는 검사한 타입이 조건에 맞는다는 것을 알므로, 평범한 T에는 message 속성이 없는데도 위의 T["message"]가 허용됩니다.
이어지는 삼항 연산자처럼 중첩할 수도 있습니다.
함수 본문의 as TypeName<T>는 선택 사항이 아닙니다. 다음 절에서 그 이유를 보여 줍니다.
조건부 반환 타입에는 단언이 필요하다
반환 타입이 자신의 타입 매개변수에 대한 조건부 타입인 함수는 어느 분기도 바로 반환할 수 없습니다. TypeScript는 본문 안에서 T를 좁히지 않으므로 어느 분기가 적용되는지 알 수 없습니다.
index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.
흔한 해결책은 두 가지입니다. 입력과 출력의 쌍을 각각 밝히고 호출하는 쪽을 정확히 검사하는 오버로드, 또는 구현 안의 단언입니다.
오버로드는 함수 오버로딩 페이지에서 다룹니다. 단언을 쓰면 컴파일러가 여러분을 믿으므로, 본문에서 잘못된 분기를 반환해도 잡히지 않습니다.
분배 조건부 타입
검사하는 타입이 맨 타입 매개변수이고 유니언을 받으면, 조건이 멤버마다 한 번씩 실행되고 결과가 새 유니언으로 합쳐집니다.
분배가 Exclude와 Extract를 동작하게 합니다. Exclude<T, U>는 T extends U ? never : T로 정의됩니다. U에 맞는 멤버는 never가 되고, never는 유니언에서 사라집니다. 그래서 Exclude<"a" | "b" | "c", "a">는 "b" | "c"입니다.
같은 규칙에서 두 가지 의외의 결과가 나옵니다. boolean은 유니언 true | false이므로 ToArray<boolean>은 boolean[]이 아니라 false[] | true[]입니다. 그리고 never는 빈 유니언이므로, 분배 조건부 타입에 never를 주면 아무것도 검사하지 않고 never를 반환합니다.
type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true
type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true
infer로 타입 추출하기
infer는 extends 절 안에서 새 타입 변수를 선언합니다. 매칭에 성공하면 TypeScript가 검사한 타입에서 그 변수를 채우고, true 분기에서 그것을 쓸 수 있습니다.
T extends Promise<infer V> ? V : T는 "T가 무언가의 프로미스라면 그 무언가를 V라고 부르고 반환하고, 아니면 T를 그대로 반환하라"로 읽으세요. infer는 조건부 타입의 extends 절에서만 허용됩니다.
infer 변수는 extends로 자체 제약을 가질 수 있습니다. 그러면 추론된 타입이 맞을 때만 매칭에 성공합니다.
type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string
ReturnType 직접 만들기
내장 ReturnType은 infer를 쓰는 한 줄짜리 조건부 타입입니다. 직접 작성해 보는 것은 두 개념을 모두 이해하게 해 주는 고전적인 연습입니다.
typeof makeUser가 함수 값을 타입으로 바꾸고, 조건부 타입이 그것을 "아무 함수"와 매칭해서 반환 타입을 R로 잡아냅니다. 표준 라이브러리 버전은 두 가지 세부 사항이 다릅니다. 매개변수가 함수 타입으로 제약되어 있어서(T extends (...args: any) => any) ReturnType<string>은 never가 아니라 컴파일 오류이고, false 분기는 any입니다. ReturnType 페이지에서 Parameters, InstanceType, Awaited도 다룹니다.
재귀 조건부 타입
조건부 타입은 자기 자신을 참조할 수 있으므로, 어떤 깊이의 중첩이든 풀 수 있습니다.
type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string>; // string
내장 Awaited<T>가 이렇게 동작해서 Promise<Promise<T>>를 T까지 풉니다. 실제로는 재귀를 얕게 유지하세요. 매우 깊거나 끝없는 재귀는 컴파일러가 error TS2589: Type instantiation is excessively deep and possibly infinite로 포기하게 만듭니다.
빠른 참조
| 패턴 | 의미 |
|---|---|
T extends U ? X : Y | T가 U에 대입 가능하면 X, 아니면 Y |
T extends U ? never : T | U에 맞는 멤버를 제거(이것이 Exclude) |
T extends U ? T : never | U에 맞는 멤버를 유지(이것이 Extract) |
[T] extends [U] ? X : Y | 같은 검사, 유니언에 분배하지 않음 |
T extends (infer E)[] ? E : T | 배열의 요소 타입 |
T extends Promise<infer V> ? V : T | 프로미스의 값 타입 |
T extends (...args: any[]) => infer R ? R : never | 함수의 반환 타입 |
T extends [infer H, ...infer Rest] ? ... | 튜플의 첫 요소와 나머지 |
자주 묻는 질문
TypeScript에서 조건부 타입이란 무엇인가요?
T extends U ? X : Y 형태의 타입입니다. T가 U에 대입 가능하면 결과는 X, 아니면 Y입니다. 컴파일 시점에 평가되는 타입의 if/else이며, 출력된 JavaScript에는 아무것도 남지 않습니다.
TypeScript에서 infer 키워드는 무엇을 하나요?
infer는 조건부 타입의 extends 절 안에서 타입 변수를 선언하고, 매칭된 타입에서 TypeScript가 그것을 채우게 합니다. T extends Promise<infer V> ? V : T는 프로미스 타입에서 결과 값의 타입을 꺼냅니다. 조건부 타입의 extends 절에서만 쓸 수 있습니다.
분배 조건부 타입이란 무엇인가요?
검사하는 타입이 맨 타입 매개변수이고 유니언을 넘기면, 조건이 각 멤버에 따로 적용되고 결과가 합쳐집니다. type ToArray<T> = T extends unknown ? T[] : never에서 ToArray<string | number>는 string[] | number[]가 됩니다. 이를 끄려면 양쪽을 대괄호로 감싸세요: [T] extends [unknown].
TypeScript에서 함수의 반환 타입은 어떻게 얻나요?
내장 ReturnType<typeof fn>을 쓰세요. infer를 쓰는 조건부 타입입니다: T extends (...args: any) => infer R ? R : any. async 함수라면 프로미스 대신 결과 값을 얻기 위해 Awaited<...>로 감싸세요.
IsNever<never>가 true 대신 never를 반환하는 이유는 무엇인가요?
never는 빈 유니언이고, 분배 조건부 타입은 유니언의 멤버를 순회합니다. 멤버가 없으면 순회할 것이 없어서 결과가 never입니다. never 자체를 검사하려면 [T] extends [never] ? true : false라고 쓰세요.