블로그 목록

조건부 타입과 infer — 타입을 계산하는 법

2026-06-247 min read
TypeScript조건부 타입infer타입 시스템

ReturnType<T>는 어떻게 함수의 반환 타입을 알아낼까? 답은 조건부 타입 + infer다. TypeScript가 "타입을 계산하는" 방식을 단계적으로 본다.

1. 조건부 타입 — 타입 수준의 if

T extends U ? X : Y 형태로 타입을 조건에 따라 분기한다.

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<number>;  // false

T extends U는 "T가 U에 할당 가능한가(서브타입인가)"를 묻는다. 값의 if가 아니라 타입의 if다.

2. infer — 조건 안에서 타입을 "추출"

infer는 조건부 타입의 extends 절 안에서, 매칭되는 부분의 타입을 변수로 뽑아내는 키워드다. "이 자리에 들어있는 타입을 R이라고 부를게"라고 선언하는 것과 같다.

infer — 패턴에서 "그 자리의 타입"을 R로 추출한다(...args: any[]) =>infer R? R : never입력() => User결과R = User반환 자리 매칭R에 담김ReturnType · Parameters · Awaited 가 모두 이 infer 추출로 만들어진다
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = () => { id: number; name: string };
type R = MyReturnType<Fn>; // { id: number; name: string }

동작 방식:

  • T가 "어떤 인자를 받아 무언가를 반환하는 함수" 모양에 맞는지 검사
  • 맞으면 그 무언가(반환 타입)를 R에 담아 반환, 안 맞으면 never

이게 내장 ReturnType<T>의 실제 구현이다.

3. infer는 어디서든 "그 자리"를 뽑는다

infer는 함수 반환뿐 아니라 타입 구조의 어떤 위치든 꺼낼 수 있다. 패턴은 항상 같다 — "이런 모양이면, 그 안의 ___ 자리를 꺼낸다."

// 첫 번째 매개변수
type FirstArg<T> = T extends (first: infer P, ...rest: any[]) => any ? P : never;
type P = FirstArg<(name: string, age: number) => void>; // string

// 배열의 요소
type ElementType<T> = T extends (infer E)[] ? E : never;
type E = ElementType<number[]>; // number

// Promise 내부
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type V = Unwrap<Promise<string>>; // string

// 객체 속성
type PropType<T> = T extends { value: infer V } ? V : never;
type W = PropType<{ value: number }>; // number

4. 분배 조건부 타입 (Distributive) — 유니온에서 자동 반복

조건부 타입의 T가 유니온이고 "벗겨진(naked)" 타입 파라미터면, 유니온의 각 멤버에 개별 적용한 뒤 다시 합친다. 의외로 헷갈리는 핵심 동작이다.

type ToArray<T> = T extends any ? T[] : never;

type R = ToArray<string | number>;
// 분배: ToArray<string> | ToArray<number>
// = string[] | number[]   ❗ (string | number)[] 가 아님

Exclude도 이 분배 덕분에 동작한다.

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

type R = MyExclude<"a" | "b" | "c", "a">;
// ("a"→never) | ("b"→"b") | ("c"→"c") = "b" | "c"

분배를 끄려면 []로 감싸 벗겨진 상태를 막는다.

type NoDistribute<T> = [T] extends [any] ? T[] : never;
type R = NoDistribute<string | number>; // (string | number)[]

5. 실전 — 내장 유틸리티의 정체

이제 내장 유틸리티들이 어떻게 만들어졌는지 읽을 수 있다.

type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : never;
type NonNullable<T> = T extends null | undefined ? never : T; // + 분배

6. 심화 ① — 재귀 조건부 타입

조건부 타입은 자기 자신을 다시 호출할 수 있다. 중첩 Promise를 끝까지 벗기는 Awaited가 그 예다.

type DeepAwaited<T> = T extends Promise<infer V> ? DeepAwaited<V> : T;
type R = DeepAwaited<Promise<Promise<string>>>; // string

// 배열 평탄화
type Flatten<T> = T extends (infer E)[] ? Flatten<E> : T;
type F = Flatten<number[][][]>; // number

7. 심화 ② — infer + 템플릿 리터럴

문자열 패턴에서 부분을 뽑을 수도 있다.

type Greeting<S> = S extends `Hello, ${infer Name}!` ? Name : never;
type N = Greeting<"Hello, 수빈!">; // "수빈"

// 라우트 파라미터 추출
type Param<S> = S extends `/users/${infer Id}` ? Id : never;
type Id = Param<"/users/42">; // "42"

8. 심화 ③ — infer 제약 (TS 4.7+)

추출하면서 동시에 타입을 제약할 수 있다.

type FirstString<T> = T extends [infer F extends string, ...any[]] ? F : never;
type R = FirstString<["hi", 1, 2]>; // "hi"

9. 정리

도구역할
조건부 타입 T extends U ? X : Y타입 수준의 if 분기
infer조건 안에서 매칭되는 부분의 타입을 추출
분배 동작유니온이면 멤버별로 자동 반복 후 합침
재귀조건부 타입이 자기 자신 호출 (깊은 구조 처리)

한 줄 정리 — 조건부 타입은 타입을 조건으로 분기하고, infer는 그 조건 안에서 "이 자리의 타입"을 꺼낸다. 둘을 합치면 함수 반환·배열 요소·Promise 내부를 추출하고, 재귀·템플릿 리터럴과 결합해 복잡한 타입 변환까지 만들 수 있다. ReturnType·Parameters·Awaited가 전부 이 조합이다.


기본 유틸리티 타입과 매핑 타입은 TypeScript 유틸리티 타입 글에서 다룬다.