TypeScript 조건부 타입 이해하기, Conditional Type

date
May 5, 2023
slug
typescript-conditional-type
summary
TypeScript의 조건부 타입에 대해서
thumbnail
status
publish

조건부 타입, Conditional Type

type Type = T extends U ? X : Y;
타입스크립트의 조건부 연산은 삼항 연산자 조건문같은 형태을 띄고있다. extends를 기준으로 왼쪽 타입(T) 이 오른쪽 타입(U) 에 할당이 가능하다면 X, 아니면 Y이다.
한마디로 우리가 알던 삼항 연산자에서 타입으로 조건문의 결과를 얻게 해준다.

예시

interface Animal {} interface Person {} type AnimalAndPersonKey = 'dog' | 'me' | 'cat'; type AnimalAndPerson<T extends AnimalAndPersonKey> = { [K in T]: K extends 'me' ? Person : Animal; }; const Object: AnimalAndPerson<AnimalAndPersonKey> = { dog: {}, me: {}, cat: {}, };
위 예시에서는 제네릭 안 extends을 통해서는 타입 제한을 걸었다.
그 후 반복되는 타입들을 지정하면서 조건부 타입을 통해 사용자가 제네릭으로 넘겨준 Key의 타입에 따라서 Value에 "me" 일 경우 Person 타입, 그 외는 Animal 타입으로 지정 할 수 있다.

분산 조건부 타입, Distributive Conditional Types

조건부 타입에는 한가지 특징이 존재한다.
조건부 타입에서 제네릭이 유니온 타입을 만나면 분산적으로 동작한다는 것이다.
말로 설명하면 이해가 잘 안되서 예시를 바로 보는것이 낫다. 😎

예시

type ToArray<T> = T extends any ? T[] : never; type Result = ToArray<string | number>;
위와 같은 예시에서 Result의 값은 어떻게 될까?
❌ type Result = (string | number)[]; ✅ type Result = string[] | number[];
❌ 라고 생각할 수 있지만, 분산 조건부 타입으로 인해 결과적으로 ✅ 같이 된다. 유니온 타입으로 지정된 타입을 하나씩 분산(순회)하면서 조건부 타입에 대입하기 때문이다.
type Result = | (string extends any ? string[] : never) | (number extends any ? number[] : never);
조건부 타입에서 never는 해당 타입을 제외시킨다는 의미로 사용된다.

응용

이러한 조건부 타입을 응용해서 TypeScript의 유틸리티 타입을 직접 만들어 볼 수 있다. 실제로 내부적으로 유틸리티 타입들은 조건부 타입을 사용하고 있다.

Exclude

type MyExclude<T, U> = T extends U ? never : T;

Omit

type MyOmit<T, K> = { [key in keyof T as key extends K ? never : key]: T[key]; };

infer 추론 타입

type Type = T extends infer U ? X : Y;
infer 키워드는 조건부 타입에서 extends 절에서 사용가능한 키워드이다.
infer를 통해 T로 들어온 타입 혹은 T의 일부 타입을 U로 할당해준다. 할당된 U는 조건부 로직의 결과나 과정에 사용이 가능하게 된다.

예시

type ReturnType<T> = T extends (...args: never[]) => infer R ? R : never; type Num = ReturnType<() => number>; // number type Str = ReturnType<(x: string) => string>; // string
예시에서 조건부 연산은 T가 함수 형태인지 확인한다.
() => number () => infer R
그 후 infer R를 통해 T로 들어온 함수의 결과 값(Return) 을 알아서 추론하여 R에 담게 된다. 그 후 T가 함수이면 함수의 결과 타입을 반환하는 타입이 된다.
ReturnType이란 타입은 함수의 결과값에 대한 타입 정보를 모른다. 하지만 infer를 통해 들어온 T 함수의 결과를 추론해서 타입을 생성하게 된다.
type ReturnType<T> = T extends (...args: never[]) => any => string | number : never;
만약 infer를 사용하지 않는다면 유니온 타입을 직접적으로 명시하여 함수의 결과값 타입을 알 수 있을 것이다. 하지만 함수가 조금만 더 복잡해지거나 오버로딩 된다면? 지속적으로 ReturnType을 수정해줘야 할 것이다. 🥹