Post

TypeScript 의 조건문

이번엔 타입스크립트에서 타이핑할 때 사용할 수 있는 컨디셔널 타입(Conditional Type)에 대해 알아보도록 하자.

컨디셔널 타입(Conditonal Type)

1
2
3
4
5
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 키워드를 사용했다. 자바스크립트에 있는 삼항연산자를 떠올리면 이해가 쉽다.

타입 B1 의 경우, A1 이 string 에 대입이 가능하다면 number, 그렇지 않다면 boolean 이다. B1 은 조건을 만족하기 때문에 number 타입이고, B2 는 만족하지 못해 boolean 이다.

조금 더 복잡한 예시를보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface X {
    x: number;
}

interface XY {
    x: number;
    y: number;
}

interface YX extends X {
    y: number;
}

type A = XY extends X ? string : number;
type B = YX extends X ? string : number;

이 상황에서, type A 와 type B 는 어떤 타입으로 평가될까? 일단, type B 는 명시적으로 X 를 extends 하니 바로 string 이라고 말할 수 있을 것이다.

사실, type A 또한 string 이다. 인터페이스 XY 는 인터페이스 X 에 대입이 가능하다. XY 보다 X 가 더 넓은 타입이기 때문이다. 이는 명시적으로 extends 한다는 것과 동일하게 처리한다.

컨디셔널 타입을 통한 타입 검사

1
2
type Result = 'hi' extends string ? true : false; // type Result = ture
type Result2 = [1] extends [string] ? true : false; // type Result2 = false

이런 방식으로 타입을 만족하는지 아닌지 검사할 수 있다.

제네릭의 타입검사를 할 때, never 와 함께 사용할 수 있다.

1
2
3
type ChooseArray<A> = A extends string ? string[] : never;
type StringArray = ChooseArray<string>; // type StringArray = string[]
type Never = ChooseArray<number>; // type Never = never

never 는 모든 타입에 대입할 수 있으니, 모든 타입을 extends 한다.

컨디셔널 타입 분배법칙

컨디셔널 타입, 제네릭과 never 의 조합을 통해 조금 더 복잡한 상황을 해결할 수 있다.

stringnumber 타입이 있을 때, 이를 통해 string[] 타입을 유도해내는 과정이다.
1
2
type Start = string | number;
type Result = Start extends string ? Start[] : never; // type Result = never

위처럼 사용한다면, string | number 는 string 에 대입할 수가 없어 extends 할 수 없기에 never 를 얻게 된다. 제네릭과 함께 사용해서 해결해보자.

1
2
3
type Start = string | number;
type Result<Key> = Key extends string ? Key[] : never;
let n: Result<Start> = ['hi']; // let n: string[]

이게 가능한 이유는, 분배법칙 때문이다. 검사하려는 값이 제네릭이면서, 유니언이면 분배법칙이 실행된다. 현재 Result<Start>Result<string | number> 는 분배법칙에 의해 Result<string> | Result<number> 가 된다.

이어서, Result<string> 은 string[] 를 반환하고, Result<number> 는 never 가 되어 사라지게 되니 최종적으론 string[]만 남는다.

분배법칙을 막는 방법

때에 따라서는 분배법칙이 일어나는 것을 막고 싶을 때가 있을 수 있다. boolean 은 분배법칙이 적용되면 조금 골치아프다.

1
2
3
4
type Start = string | number | boolean;
type Result<Key> = Key extends string | boolean ? Key[] : never;
let n: Result<Start> = ['hi']; // let n: string[] | false[] | true[]
n = [true];
string[]boolean[] 을 예상했는데, string[]false[]true[] 가 되었다.
boolean 이 falsetrue 로 나뉘어버렸기 때문이다.  

위 예시에서 분배법칙을 막아보자.

1
2
3
type Start = string | number | boolean;
type Result<Key> = [Key] extends [string | boolean] ? Key[] : never;
let n: Result<Start> = ['hi']; // Type 'string[]' is not assignable to type 'never'.
stringnumberboolean 은 stringboolean 에 대입이 불가능하다. stringboolean 이 더 좁은 타입이기 때문이다.

아래 다른 예시를 통해 살펴보자.

1
2
3
4
5
type IsString<T> = T extends string ? true : false;
type Result = IsString<'hi' | 3>; // type Result = boolean

type IsString<T> = [T] extends [string] ? true : false;
type Result = IsString<'hi' | 3>; 
이 예시에서도 마찬가지로, 리터럴 유니온 타입 ‘hi’3 은 string 에 대입할 수 없다.

마지막으로, 제네릭과 never 에 대해 알아보자.

1
2
type R<T> = T extends string ? true : false;
type RR = R<never>; // type RR = never

앞에서 never 는 모든 타입을 extends 할 수 있다고 했는데, 왜 RR 은 never 인 것일까? 이는 never 에도 분배법칙이 적용 되기 때문이다.

never 에 어떻게 분배법칙이 적용되는지 혼란이 올 수 있는데, 컨디셔널 타입 + 제네릭 + never === never 쯤으로 기억해두는 게 좋다. 물론 분배법칙을 막는다면, never 를 인수로 사용할 수 있다.

1
2
3
type IsNever<T> = [T] extends [never] ? true : false;
type T = IsNever<never>; // type T = true
type F = IsNever<'never'>; // type F = false
This post is licensed under CC BY 4.0 by the author.