never도 분배법칙의 대상이 된다. -> never는 유니언으로 보이진 않지만 유니언으로 생각하는것이 좋다.
never는 분배법칙이 일어나면 never 가 된다.
간단하게 제너릭과 never가 만나면 never가 된다고 생각하자.
never를 타입인수로 사용할 때는 분배법칙을 막기 위해 배열로 감싸라
타입스크립트는 제너릭이 들어있는 컨디셔널 타입을 판단할 때 값의 판단을 뒤로 미룬다.
이 때도 타입스크립트가 판단을 뒤로 미루지 못하도록 배열로 제너릭을 감싸면 된다.
컨디셔널 타입
조건에 따라 다른 타입이되는 컨디셔널 타입
type A1 = string;
type B1 = A1 extends string ? number : boolean; // type B1 = number
type A2 = number;
type B2 = A2 extends string ? number : boolean; // type B2 = boolean
extends 연산자가 삼항연산자와 같이 사용됨
특정 타입 extends 다른 타입 ? 참일 때 타입 : 거짓일 때 타입
특정 타입이 다른 타입의 부분집합일 때 참
컨디셔널 타입은 타입 검사를 위해서도 많이 사용함
컨디셔널 타입은 never와 함께 사용할 때 도 많다.
보통은 제너릭과 더불어 쓸 때만 never가 의미가 있다.
type Start = string | number;
type New = Start extends string | number ? Start[] : never;
let n: New = ['hi'];
n = [123];
❓ 그냥 type New = Start[] 로 사용해도 되지 않나??
사실 그렇다.
단순한 상황에서는 never 와 함께 쓸 이유가 없다.
보통은 제너릭과 더불어 쓸 때만 never가 의미가 있다.
type ChooseArray<A> = A extends string ? string[] : never;
type StringArray = ChooseArray<string>; // type StringARray = string[]
type Never = ChoosArray<number>; // type Never = never;
never 타입은 모든 타입에 대입할 수 있기에 모든 타입을 extends 할 수 있다.
type Result = never extends string ? true : false; // type Result = true
매핑된 객체 타입에서 키가 never 이면 해당 속성은 제거된다.
type OmitByType<O, T> = {
[K in keyof O as O[K] extends T ? never : K]: O[K];
}
type Result = OmitByType<{ // type Result = { name: string, age: number }
name: string;
age: number;
married: boolean;
rich: boolean;
}, boolean>
중첩 삼항연산자
컨디셔널 타입은 자바스크립트 삼항연산자처럼 중첩해서 만들기 가능
type ChooseArray<A> = A extends string
? string[]
: A extends boolean ? boolean[] : never;
type StringArray = ChooseArray<string>; // string[]
type BooleanArray = ChooseArray<boolean>; // boolean[]
type Never = ChooseArray<number>; // never
인덱스 접근 타입으로 컨디셔널 타입을 표현 가능
type A1 = string;
// B1 타입과 B2 타입은 같음
type B1 = A1 extends string ? number : boolean;
// B2 처럼 왜 굳이 복잡하게 사용하냐면, 참일 때와 거짓일 때의 타입이 복잡한 경우는 아래처럼 나타내기도 함
type B2 = {
't': number;
'f': boolean;
}[A1 extends string ? 't' : 'f']
분배 법칙
제너릭과 never의 조합은 더 복잡한 상황에서 진가를 발휘함
string | number 타입으로부터 string[] 타입을 얻고 싶을 때?
type Start = string | number;
// string | number 가 string을 extends 할 수 없기 때문
// string이 더 구체적이므로 대입 불가능
type Result = Start extends string ? Start[] : never; // never
// 컨디셔널 타입을 제너릭과 함께 사용하면 원하는 동작 가능
type Start = string | number;
type Result<Key> = Key extends string ? Key[] : never;
let n: Result<Start> = ['hi']; // let n: string[]
검사하려는 타입이 제너릭이면서 유니언이면 분배법칙이 실행됨
Result<string | number>는 Result<string> | Result<number> 가 된다.
따라서 Key extends string | boolean ? Key[] : never를 거치면서 string [] | never가 되고 never는 사라져서 최종적으로 string[] 타입이 된 것
boolean에 분배법칙이 적용될 떄는 조심!!
boolean을 true | false 로 인식하게 된다.
type Start = string | number | boolean;
type Result<Key> = Key extends string | boolean ? Key[] : never;
let n: Result<Strart> = ['hi']; // string[] | false[] | true[]
n = [true];
분배법칙을 막고 싶다면?
배열로 감싸면 분배법칙이 일어나지 않는다.
type IsString<T> = T extends string ? true : false;
// 분배법칙에 의해 IsString<'h1'> | IsString<3> -> true | false => boolean
type Result = IsString<'h1' | 3>; // type Result = boolean
// 배열로 감싼 경우
type IsString<T> = [T] extends [string] ? true : false;
type Result = IsString<'h1' | 3>; // type Result = false
never도 분배법칙의 대상이 된다.
never가 유니언으로 보이지는 않지만 유니언으로 생각하는것이 좋다.
never가 분배법칙이 일어나면 never가 된다. (공집합에서 분배법칙이 일어나면 아무것도 실행하지 않기때문)
type R<T> = T extends string ? true : false;
// 분배법칙이 일어나서 true가 아니라 never가 됨
type RR = R<never>; // type RR = never
never는 공집합과 같으므로공집합에서 분배법칙을 실행하는 것은 아무것도 실행하지 않는것과 같다.
간단하게 제너릭과 never가 만나면 never가 된다고 생각하자.
따라서 never를 타입인수로 사용하려면 분배법칙이 일어나는것을 막아야 한다.
type IsNever<T> = [T] extends [never] ? true : false;
type T = isNever<never>; // Type T = true
type F = isNever<'never'>; // Type F = false
같은 이유로 제너릭과 컨디셔널 타입을 같이 사용할 때는 다음을 조심하자.
타입스크립트는 제너릭이 들어있는 컨디셔널 타입을 판단할 때 값의 판단을 뒤로 미룬다.
이 때도 타입스크립트가 판단을 뒤로 미루지 못하도록 배열로 제너릭을 감싸면 된다.
function test<T>(a: T) {
type R<T> = T extends string ? T : T; // R<T> 타입이 T 타입이 될거라고 생각하는게 잘못됨
// 즉, 변수 b에 매개변수 a를 대입할 때까지도 타입스크립트는 R<T>가 T 라는것을 알지 못한다.
const b: R<T> = a; // type 'T' is not assignable to type 'R<T>'
}
// 제너릭을 배열로 감싸면 타입 판단을 뒤로 미루지 않게됨
// 타입 매개변수를 선언할 때 바로 <[T] extends [string]> 하는것이 불가능하므로
// 한 번 더 컨디셔널 타입으로 묶어서 선언한 것
function test<T extends ([T] extends [string] ? string : never)>(a: T) {
type R<T> = [T] extends [string] ? T : T;
const b: R<T> = a;
}