조건부 타입(condition Type) + infer

삼항 연사자처럼 타입도 조건에 따라 타입이 달라지게 프로그래밍 할 수는 없을까?

Condition Type

타입도 삼항 연산자와 유사한 방식으로 조건부 타입을 타입핑할 수 있다.

T extends U ? X : Y

이것을 간단히 리뷰해 보자면, **"T가 U에 할당될 수 있는지를 확인하고 이것이 참이라면 X 타입을 사용하고 그렇지 않다면 Y 타입을 사용해라"**라는 의미다. 예제를 통해 확인하면 이해가 될 것이다.

interface IItem {
  id: number;
}

function getItems<T>(id?: T): T extends number ? IItem : IItem[] {
  if (typeof id === "number") {
    return { id: 1 } as any;
  }
  return [{ id: 1 }, { id: 2 }] as any;
}

const result1 = getItems(1); // Type is IItem
const result2 = getItems(); // // Type is IItem[]

왜 any를 사용했지?

위 코드를 보면 any를 사용했는데, 이유는 타입스크립트가 타입 유추를 하지 않도록 타입 단언(type assertion) as와 any를 사용했다. 왜냐하면 id 타입이 조건부 타입으로 선택되는 것이 아니므로 함수가 조건문을 평가할 수 없고 Item 타입으로 타입을 좁힐 수 없기 때문이다.

Infer

infer 키워드를 조건부 타입에서 사용하면 실제 분기의 비교할 유형에서 추론하는 방법을 제공해 줄 수 있다.

type GetReturnType<T> = T extends (...args: never[]) => infer R ? R : never;

type Num = GetReturnType<() => number>; // type is number
type Str = GetReturnType<(x: string) => string>; // type is string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // type is boolean[]

조금 더 예제를 확장해 보면 아래와 같이 작성할 수 있다.

type ReturnPromise<T> = T extends (...args: infer A) => infer R ? (...args: A) => Promise<R> : T;

type Promisify<T> = {
  [P in keyof T]: ReturnPromise<T[P]> // ref: Mapped Types
}

interface IItemService {
  id: string;
  getItem(): string;
  getItems(): string[];
}

class ItemService implements Promisify<IItemService> {
  id: string = '';

  getItem() { // type is Promise<string>
    return Promise.resolve('')
  }

  getItems() { // type is Promise<string[]>
    return Promise.resolve([''])
  }
}

type ReturnPromise = T extends (...args: infer A) => infer R ? (...args: A) => Promise : T;

마지막으로 위 코드만 풀어서 해석하자면 다음과 같다.

  • ReturnPromise 타입은 제네릭 타입이며,

  • A 타입으로 추론되는 여러 개의 파라미터를 가지며, 리턴 타입이 R로 추론되는 함수 (...args: infer A) => infer R

  • T 타입이 할당될 수 있다면

  • (참일 경우, 타입이 추론 되었기 때문에 타입을 확정할 수 있다) (...args: A) => Promise<R>의 타입을 가지며

  • 그렇지 않을 경우 T 타입을 갖는다. (위의 코드에서 id가 프로미스로 랩핑되지 않고 그대로 string 타입을 갖는 이유이다)

Last updated