Pandaman Blog

[Typescript] Conditional Types 본문

Front end/Typescript

[Typescript] Conditional Types

oyg0420 2022. 3. 13. 17:49

Conditional Types

Conditional Types은 조건에 따라서 타입 변수에 할당할 수 있는 타입을 결정할 수 있다.

T extends U ? X : Y

Conditional Type은 3항 연산자 구문을 사용하고 있다. T extends U 조건이 충족되면 타입 X가 되고, 그렇지 않으면 타입 Y가 된다.

한번 예제를 살펴보자.

interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}

type Example1 = Dog extends Animal ? number : string;

Dog 인터페이스는 Animal를 확장해서 생성한 타입이다. Example1를 확인해보면 Dog extends Animal가 true 면 number타입이고, false면 string가 된다. Dog는 Animal를 extends 했기때문에 Example1 타입은 number가 된다.
여기서 주목할 점은 제약 조건(extends Animal ?)을 추가함으로써 타입을 결정할 수 있다는 점이 되겠다. 사실 위의 예제보다는 제네릭을 통해서 타입을 결정할때 유용하게 사용될 수 있다.

generic에서 conditional types

제네릭을 통해서 우리는 conditional type을 유용하게 사용할 수 있다.
링크

type StringOrNumber<T extends string | number> =  T extends string ? 'this is string' : 'this is number';

type MustBeString = StringOrNumber<'hi'>;

type MustBeNumber = StringOrNumber<123>;

StringOrNumber 타입의 타입인자 T에는 string 또는 number 타입을 할당할 수 있다. 그리고 조건부를 확인해보면 T 가 string 가 참인 경우 'this is string'인 리터럴 타입이되고 아니라면 'this is number'인 리터럴 타입이 된다.

지난 시간에 타입을 순회하면 새로운 타입을 반환하는 Mapped type을 배웠다. 아래는 Mapped type을 사용한 코드이다.

interface Person {
 경력: number;
 취미: string;
}

type FrontChapter = "bk" | "lio" | "blue" | "dali" | "coo";

const frontChapter: RecordTest<FrontChapter, Person> = {
  bk: { 경력: 10, 취미: "게임" },
  lio: { 경력: 9, 취미: "독서" },
  blue: {경력: 8, 취미: "축구"},
  dali: { 경력: 7, 취미: "헬스" },
  coo: { 경력: 6, 취미: "청소" },
};

type RecordTest<K extends string , T> = {
    [P in K]: T;
};

여기서 FrontChapter 에 poodle 리터럴 타입을 추가하고 싶다. 속성이 poodle 인 경우는 털빠짐, 종류 속성을 갖는 타입 Animal이 되도록 하고 나머지는 기존과 동일하게 Person 타입을 갖게 하고 싶다.
링크

interface Person {
 경력: number;
 취미: string;
}

interface Animal {
  털빠짐: '많음' | '보통' | '적음';
  종류: string;
}

type FrontChapter = "bk" | "lio" | "blue" | "dali" | "coo" | 'poodle';

const frontChapter: RecordTest<FrontChapter> = {
  bk: { 경력: 10, 취미: "게임" },
  lio: { 경력: 9, 취미: "독서" },
  blue: {경력: 8, 취미: "축구"},
  dali: { 경력: 7, 취미: "헬스" },
  coo: { 경력: 6, 취미: "청소" },
  poodle: { 털빠짐: '적음', 종류: '푸들' }
};

type RecordTest<K extends string> = {
    [P in K]: P extends 'poodle' ? Animal: Person;
};

이전과 다른점은 P extends 'dog'를 추가했다는 점이다. 속성 키 타입인 Ppoodle 라는 조건에서 참인 경우 Animal가 되고 아닌 경우 Person가 된다. Mapped typeConditional type은 이렇게 조합하여 자유롭게 사용가능하다.

Distributive Conditional Types(분배 조건부 유형)

조건부 형식이 제네릭 타입에서 작동할 때 유니온 타입이 지정되면 Distributive type 이 된다고 한다. 음 이게 무슨말일까.
순서대로 확인해보자.

  1. T extends U ? X : Y 가 있다고 가정
  2. 매개변수 타입 T 는 A | B | C 유니온 타입이 전달된다고 가정
  3. 다음과 같이 해석된다. (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

위와 같은 순서대로 Distributive type 동작이 동작한다.

예제를 살펴보며 다시 Distributive type 동작을 살펴보자.

type NonNullable<T> = T extends null | undefined ? never : T;

NonNullable 타입은 타입인자 T를 갖으며 T가 null | undefined 라면 never가 되고 아니라면 T를 그대로 반환한다.
여기서 never 타입은 T extends null | undefined 조건이 참인 경우 대한 타입을 제거하는데 사용된다.

type PhoneNumberTypes = number | number[] | null | undefined;
type NonNullableEmailAddress = NonNullable<PhoneNumberTypes>;

다음과 같이 맵핑된다.

type NonNullablePhoneNumberTypes =
  | NonNullable<number>
  | NonNullable<number[]>
  | NonNullable<null>
  | NonNullable<undefined>;

다시 NonNullable 타입을 풀어서 작성하면

type NonNullablePhoneNumberTypes =
  | (number extends null | undefined ? never : number)
  | (number[] extends null | undefined ? never : number[])
  | (null extends null | undefined ? never : null)
  | (undefined extends null | undefined ? never : undefined);

가 된다.

type NonNullablePhoneNumberTypes = number | number[] | never | never;

never는 가장 하위 타입이므로 never 에는 어떤 타입도 할당할 수 없다. 즉 타입 NonNullablePhoneNumberTypesnumber | number[] 타입만 할당가능한 타입이다. 따라서 never 타입은 유니온타입에서 생략가능하게 된다.

type NonNullablePhoneNumberTypes = number | number[];

따라서 결론적으로 위의 타입을 얻을 수 있다.

infer keyword

위에서는 조건부 결과에 의한 타입추론에 대한 설명이었다면, 이번에는 조건부 내에서의 타입을 추론하기 위해 infer 키워드를 사용해야한다.
그럼 조건부내에서 타입 추론을 할 필요가 있을까. 라는 생각이 들었다.
결론은 필요하다.

type GetReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer Return
  ? Return
  : never;

GetReturnType 타입은 인자로 전달한 함수의 반환 타입을 반환하는 타입이다.

T extends (...args: any) => infer Return ? Return : never

infer 키워드 뒤의 타입 Return는 타입매개변수 T의 반환타입을 추론한 타입이다. T extends (...args: any) => infer Return 조건에 참이 된다면 타입 Return 반환한다.

type NumberReturnType = GetReturnType<() => number>;
// ?


type StringReturnType = GetReturnType<(x: string[]) => string>;
// ?

type BoolsReturnType = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// ?

예상대로 모두 타입매개변수의 반환타입을 반환한다.

'Front end > Typescript' 카테고리의 다른 글

[Typescript] Utility Types - 2  (0) 2022.03.27
[Typescript] Mapped Types  (0) 2022.02.24
[Typescript] Utility Types - 1  (0) 2022.02.21
[Typescript] 함수  (0) 2021.12.19
[Typescript] Type Guard  (0) 2021.12.18
Comments