Javascript

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

드리프트2 2025. 3. 13. 22:03

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>]
>>;