조건부 타입은 extends
와 삼항 연산자를 이용해 조건에 따라 각각 다른 타입을 정의하도록 돕는 문법입니다.
type A = number extends string ? number : string;
위 조건부 타입은 number extends string ?
같은 조건식이 있고 조건식이 참이라면 number
를 반환, 아니라면 string
을 반환합니다.
따라서 위 조건부 타입의 결과는 string
타입이 됩니다.
type ObjA = {
a: number;
};
type ObjB = {
a: number;
b: number;
};
type B = ObjB extends ObjA ? number : string;
위 조건부 타입의 결과는 number
타입입니다.
조건부 타입은 제네릭과 함께 사용할 때 위력이 극대화 됩니다.
type StringNumberSwitch<T> = T extends number ? string : number;
let varA: StringNumberSwitch<number>;
// string
let varB: StringNumberSwitch<string>;
// number
varA
의 타입은number
타입이 되고 varB
의 타입은 string
타입이 됩니다.
조건부 타입과 제네릭을 함께 사용할 때 제네릭 타입이 union 타입일 경우 분산 조건부 타입(조건부 타입 분배법칙)이 됩니다.
다음은 조건부 타입의 타입 변수에 number | string
타입을 할당한 경우입니다.
type StringNumberSwitch<T> = T extends number ? string : number;
(...)
let c: StringNumberSwitch<number | string>;
// string | number
타입 변수에 할당한 union 타입 내부의 모든 타입이 분리되어 분산됩니다.
StringNumberSwitch<number>
StringNumberSwitch<string>
그 후에 분산된 각 타입의 결과를 모아서 다시 union 타입으로 묶습니다.
never
타입은 union으로 묶일 경우 사라집니다. 공집합과 다른 집합의 합집합은 아무 의미가 없기 때문입니다.
number | string
분산 조건부 타입(조건부 타입 분배법칙)에서 boolean
을 사용할 경우는 주의해야 합니다.
type Start = string | number | boolean;
type Result<Key> = Key extends string | boolean ? Key[] : never;
let n : Result<Start> = ['hi'];
n = [true];
n
의 타입이 string[] | boolean[]
이 될 것을 예상하였지만 string[] | true[] | false[]
가 되었습니다.
boolean
을 true | false
로 인식하기 때문입니다.
다음도 분산 조건부 타입(조건부 타입 분배법칙)을 이용한 타입입니다.
type IsString<T> = T extends string ? true : false;
type Result = IsString<'hi' | 3>;
// type Result = boolean
분산 조건부 타입이 수행되면 true | false
가 결과가 됩니다. 따라서 결과적으로 Result
의 type은 boolean
이 되었습니다.
never
도 분배 법칙의 대상이 됩니다. never
도 union 타입이라고 생각하는 것이 좋습니다.
type R<T> = T extends string ? true : false;
type RR = R<never>;
// type RR = never;
never
타입을 분산 조건부 타입(조건부 타입 분배법칙)에 사용할 때는 결과가 never
타입이 된다고 외워두는 것이 좋습니다.
위의 예제를 그대로 들고 왔습니다.
type IsString<T> = [T] extends [string] ? true : false;
type Result = IsString<'hi' | 3>;
// type Result = false
['hi' | 3]
은 [string]
타입의 하위 타입이 아니기 때문에 조건부 타입의 결과로 false
가 됩니다.
type IsNever<T> = [T] extends [never] ? true: false;
type T = IsNever<never>;
// type T = true
type F = IsNever<'never'>;
// type F = false
제네릭 타입을 배열로 감싸주어 분산 조건부 타입(조건부 타입 분배법칙)이 발동되지 않도록 하였습니다.
[never]
는 [never]
타입의 하위 타입이기 때문에 type T
는 true
가 됩니다.
분산 조건부 타입의 특징을 이용하여 union 타입으로부터 특정 타입만 제거하는 Exclude 타입을 정의할 수 있습니다.
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<number | string | boolean, string>;
Exclude<number, string>
Exclude<string, string>
Exclude<boolean, string>
number
never
boolean
number | never | boolean
never
는 공집합이므로 공집합과 어떤 집합의 합집합은 그대로이므로 never
는 생략됩니다.
number | boolean
infer
는 조건부 타입 내에서 특정 타입을 추론하는 문법입니다.
type ReturnType<T> = T extends () => infer R ? R : never;
type FuncA = () => string;
type FuncB = () => number;
type A = ReturnType<FuncA>;
// string
type B = ReturnType<FuncB>;
// number
ReturnType<FuncA>
처럼 타입 변수 T에 FuncA
를 할당합니다.
그리고 나서 infer는 타입변수 R이 조건부 타입을 만족할 수 있는 타입으로 알아서 추론됩니다.
() => string
함수 타입이 () => infer R
함수 타입의 서브타입이 되기 위해서 타입변수 R은 string
타입이 되면 됩니다.
이것을 해주는 것이 infer
키워드 입니다.
다음과 같이 추론이 불가능하다면 조건식을 거짓으로 판단합니다.
type ReturnType<T> = T extends () => infer R ? R : never;
type FuncA = () => string;
type FuncB = () => number;
type A = ReturnType<FuncA>;
// string
type B = ReturnType<FuncB>;
// number
type C = ReturnType<number>;
// 조건식을 만족하는 R추론 불가능
// never
number
타입이 함수 타입 () => infer R
의 서브 타입이 되는 것을 타입 변수 R이 어떤 타입이 되든 만족할 수 없습니다.
따라서 위 조건부 타입은 거짓으로 판별되어 never
타입을 반환합니다.
type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;
// 1. T는 프로미스 타입이어야 한다.
// 2. 프로미스 타입의 결과값 타입을 반환해야 한다.
type PromiseA = PromiseUnpack<Promise<number>>;
// number
type PromiseB = PromiseUnpack<Promise<string>>;
// string
위는 Promise
의 resolve 타입을 infer를 이용해 추출하는 예입니다.