컨디셔널 타입

핵심 포인트

  • 컨디셔널 타입에 never를 사용할 때가 많은데 제너릭과 같이 쓸 때에만 의미가 있다.

  • 매핑된 객체 타입에서 키가 never 이면 해당 속성은 제거된다.

  • 검사하려는 타입이 제너릭이면서 유니언이면 분배법칙이 실행됨

  • boolean은 분배법칙이 일어나면 true | false 로 인식하게 된다.

  • 분배법칙을 막고 싶다면 배열로 감싸라

  • 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;
}

컨디셔널 타입은 infer를 통해 더 강력하게 사용 가능

Last updated