TypeScript의 infer 키워드로 복합 타입에서 원하는 부분만 깔끔하게 추출하기

TypeScript의 infer 키워드로 복합 타입에서 원하는 부분만 깔끔하게 추출하기

안녕하세요. 오늘은 TypeScript의 조건부 타입(Conditional types)에서 등장하는 infer라는 키워드에 대해 자세히 알아볼까 합니다.

 

이 infer 키워드를 활용하면 다소 복잡한 복합(Compound) 타입 안에서 원하는 부분의 타입만 쏙쏙 뽑아서 깔끔하게 사용할 수가 있는데요.

이 글에서 사용하는 표기법

이 글에서는 타입의 정확성이나 추론된 타입을 간편하게 확인하기 위해 asserttt라는 npm 패키지를 사용한 코드 예제를 활용할 건데요.

 

예를 들면 이런 식입니다.

// 값의 타입 확인
assertType<string>('abc');
assertType<number>(123);

// 타입의 동등성 확인
type Pair<T> = [T, T];
type _ = Assert<Equal<
  Pair<string>, [string, string]
>>;

infer 키워드란? 조건부 타입의 extends 절에서 타입 추출하기

infer 키워드는 조건부 타입(Conditional types)의 extends 절 내에서 타입을 추출할 때 쓰는 키워드입니다.

 

조건부 타입은 보통 이런 형태를 갖는데요.

type _ = Type extends Constraint ? <then> : <else>;

 

이때 Constraint(제약 조건)는 하나의 타입 패턴(Type Pattern)일 수 있습니다. 예를 들면 다음과 같습니다.

  • T[]
  • Promise<T>
  • (arg: T) => R

기존 타입 변수(위 예시의 T나 R)를 사용할 수 있는 위치라면 어디든지 infer를 이용해 새로운 타입 변수를 선언할 수 있는데요.

 

예를 들어 배열 타입에서 배열 내부의 요소 타입을 추출하는 경우를 살펴보겠습니다.

type ElemType<Arr> = Arr extends Array<infer Elem> ? Elem : never;

// 실제 타입 추출 예시
type _ = Assert<Equal<
  ElemType<Array<string>>, string
>>;

 

이런 타입 추출 방식은 JavaScript의 구조분해 할당(Destructuring)과 비슷한 느낌을 줍니다.

예시: Record 타입을 이용해 객체의 키와 값을 추출하기

내장 유틸리티 타입인 Record를 이용하면 직접 키(keyof)나 값(ValueOf)을 추출하는 타입을 구현할 수도 있습니다.

const Color = {
  red: 0,
  green: 1,
  blue: 2,
} as const;

// 키 추출 타입
type KeyOf<T> = T extends Record<infer K, any> ? K : never;
type _1 = Assert<Equal<
  KeyOf<typeof Color>,
  "red" | "green" | "blue"
>>;

// 값 추출 타입
type ValueOf<T> = T extends Record<any, infer V> ? V : never;
type _2 = Assert<Equal<
  ValueOf<typeof Color>,
  0 | 1 | 2
>>;

infer를 쓰는 TypeScript 내장 유틸리티 타입들 알아보기

TypeScript는 이미 여러 내장 유틸리티 타입을 제공합니다.

 

이 중에서도 infer를 사용하는 유틸리티 타입 몇 가지를 살펴보겠습니다.

 

함수 타입에서 infer로 타입 추출하기

// 함수의 매개변수 추출
type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

// 함수의 반환 타입 추출
type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;

 

실제 활용 예시를 들어보겠습니다.

function add(x: number, y: number): number {
  return x + y;
}

type _1 = Assert<Equal<
  typeof add,
  (x: number, y: number) => number
>>;
type _2 = Assert<Equal<
  Parameters<typeof add>,
  [x: number, y: number]
>>;
type _3 = Assert<Equal<
  ReturnType<typeof add>,
  number
>>;

클래스 타입에서 infer로 타입 추출하기

클래스 타입의 생성자 파라미터와 인스턴스 타입을 추출할 때도 infer를 활용할 수 있습니다.

type Class<T> = abstract new (...args: Array<any>) => T;

// 생성자 파라미터 타입 추출
type ConstructorParameters<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: infer P) => any ? P : never;

// 인스턴스 타입 추출
type InstanceType<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: any) => infer R ? R : any;

 

예시로 클래스 Point를 살펴볼까요?

class Point {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

const pointAsValue: Class<Point> = Point;

type _ = [
  Assert<Equal<
    ConstructorParameters<typeof Point>,
    [x: number, y: number]
  >>,
  Assert<Equal<
    InstanceType<typeof Point>,
    Point
  >>,
];

비동기 메서드를 동기 메서드로 바꾸는 예시 (고급 활용)

좀 더 복잡한 사례로, 비동기 메서드(Promise 기반)를 동기 메서드로 바꾸는 유틸리티 타입도 만들 수 있습니다.

type Syncify<Intf> = {
  [K in keyof Intf]:
    Intf[K] extends (...args: infer A) => Promise<infer R>
    ? (...args: A) => R
    : Intf[K]
};

interface AsyncService {
  factorize(num: number): Promise<Array<number>>;
  createDigest(text: string): Promise<string>;
}

type SyncService = Syncify<AsyncService>;
type _ = Assert<Equal<
  SyncService,
  {
    factorize: (num: number) => Array<number>,
    createDigest: (text: string) => string,
  }
>>;

복잡한 타입을 infer로 간단히 별칭(alias) 만들기

복잡한 타입을 만들 때, infer를 통해 일종의 타입 변수처럼 간편하게 별칭을 만들 수도 있습니다.

type WrapTriple<T> = Promise<T> extends infer W
  ? [W, W, W]
  : never;

type _ = Assert<Equal<
  WrapTriple<number>,
  [Promise<number>, Promise<number>, Promise<number>]
>>;