개발/type-challenges

타입스크립트 타입 정복: Conditional Types

JonghwanWon 2022. 7. 12. 23:14

* 이 프로젝트는 기본적으로 TypeScript를 써 보았고, 내장된 유틸리티 타입들과 Generic 등을 알고 있는 상태에서 진행하는 것을 추천합니다. 그렇지 않다면, 공식 사이트 등을 참고해가며 학습과 병행하는 것을 추천합니다.


2022.07.02 - [개발/type-challenges] - 타입스크립트 타입 정복: 내장 유틸리티 타입을 만들어보자
지난 글에서는 타입스크립트에 내장되어 자주 사용하게 되는 유틸리티 타입들을 직접 구현해보는 문제들을 소개해 드렸습니다. 어떠셨나요? 이 문제들은 type-challenges의 쉬움~보통 난이도에 해당하는 유형이었습니다.

지난 글에서 살펴본 기본적인 keyof, in, Indexed Access Types와 더불어 설명하지 못했던 Conditional Types에 대한 이해를 바탕으로 쉽게 해결할 수 있었을 것입니다.

Conditional Types

조건부 타입은 삼항 조건 연산자와 유사한 형태로 작성됩니다.

SomeType extends OtherType ? TrueType : FalseType;

extends의 왼쪽에 있는 타입(SomeType)이 오른쪽에 있는 타입(OtherType)에 할당될 수 있으면 TrueType을 얻을 수 있고 그렇지 않으면 FalsType을 얻습니다.
Conditional Types는 특히 Generic과 함께 아주 유용하게 사용될 수 있습니다.

type PickBy<T, U> = {
  [Key in keyof T as T[Key] extends U ? Key : never]: T[Key];
};

type SomeType = {
  foo: string;
  bar: boolean;
  baz: string;
};

const t1: PickBy<SomeType, string> // { foo: string; bar: string; }
const t2: PickBy<SomeType, boolean> // { bar: boolean; }


Conditional Type을 통해 Type Guard로 타입을 좁혀 구체적인 타입을 줄 수 있는 것처럼 검사하는 타입을 제한하기도 합니다.

type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];

const t3: Concat<[1], [2]> // [1, 2]
const t4: Concat<[1], 4> // Type 'number' does not satisfy the constraint 'unknown[]'

이처럼 Conditional Type을 통한 타입 유형의 검사는 조금 더 논리적으로 타입을 접근할 수 있게 합니다.

Conditional Type에서는 infer 키워드를 통해 타입을 추론해 낼 수도 있습니다. infer 키워드는 더 쉽게 타입을 추론하고 Conditional Type을 더 유용하게 사용할 수 있게 합니다.

type Flatten<T> = T extends [infer F, ...infer R]
  ? F extends Array<unknown>
    ? [...Flatten<F>, ...Flatten<R>]
    : [F, ...Flatten<R>]
  : T;
  
 const t1: Flatten<[1, [2]> // [1, 2]
 const t2: Flatten<[1, [2, 3, [4, [5]]]]> // [1, 2, 3, 4, 5]

조금 복잡할 수도 있는 Flatten 타입을 풀어 설명하면 아래와 같습니다.

1. T 가 Array인지 확인하고 첫 번째 element와 나머지를 F, R로 추론
2. F가 Array라면 Spread 하고, 나머지 R에 대해서도 Flatten을 재귀적으로 추론
3. F가 Array가 아니라면 나머지 R에 대해서만 Flatten을 재귀적으로 추론
4. T가 Array가 아니라면 받은 그대로 반환

다음은 DateFormat에 따라 Date를 분할해 반환하는 함수 타입의 예입니다.

type DateFormat = 'yyyy-MM-dd' | 'MM-dd' | 'HH:mm';
type GetKey<S> = S extends 'y'
  ? 'year'
  : S extends 'M'
  ? 'month'
  : S extends 'd'
  ? 'day'
  : S extends 'H'
  ? 'hour'
  : S extends 'm'
  ? 'minute'
  : never;

type SplitDateReturn<Format extends string> = Format extends `${infer C}${infer Rest}`
  ? { [k in GetKey<C> | keyof SplitDateReturn<Rest>]: number }
  : {};

declare function splitDate<Format extends DateFormat>(date: Date, format: Format): SplitDateReturn<Format>;

splitDate(new Date(), 'yyyy-MM-dd'); // { year: number month: number; day: number; };
splitDate(new Date(), 'MM-dd'); // { month: number; day: number };
splitDate(new Date(), 'HH:mm'); // { hour: number; minute: number };


Conditional Type은 타입스크립트를 보다 정교하게 사용할 수 있게 도와주고 논리적인 타입을 생산해내는데 유용하게 사용됩니다.
Generic과의 조합을 통해 유연하고 대응 가능한 타입도 만들 수 있으며 동시에 강력한 타입 제한을 할 수도 있습니다.

지난 글에서 살펴본 것과는 조금 어려운 부분일 수도 있지만, 꾸준히 type-challenges를 통해 연습문제를 통해 타입을 만들어 나가다 보면 금방 익숙해질 수 있을 것입니다.