타입스크립트 교재 · 12편 / 20편

조건부 타입 — T extends U ? X : Y

타입 안에 if 가 있는 듯한 강력한 도구. infer 와 분배 조건부까지.

중급읽는 시간 7분2026-05-17
T extends U ? X : Y 조건부 타입이 분기되는 트리 도식

유틸리티 타입(11편) 들이 어떻게 만들어졌나 들여다보면 거의 모두 조건부 타입이 기반입니다. 자바스크립트의 삼항 연산자(x ? a : b) 와 같은 모양 — 다만 타입 레벨에서 동작합니다. 12편은 조건부 타입과 그 동반자 infer·분배 조건부 타입까지.

기본형 — T extends U ? X : Y

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

type A = IsString<"hello">;    // true
type B = IsString<42>;          // false
type C = IsString<string>;      // true

// 실용 예 — null/undefined 제거 (NonNullable 의 정의)
type MyNonNullable<T> = T extends null | undefined ? never : T;

읽는 방법. "T 가 U 의 부분집합(할당 가능) 이면 X, 아니면 Y". extends 는 "상속" 보다는 "이것에 할당 가능" 으로 읽는 게 정확.

infer — 매치된 일부를 변수로 캡처

// 함수의 반환 타입만 뽑기 (ReturnType 의 정의)
type MyReturnType<F> = F extends (...args: any) => infer R ? R : never;

type R1 = MyReturnType<() => number>;          // number
type R2 = MyReturnType<(x: string) => User>;   // User

// Promise 안 풀기 (Awaited 의 핵심)
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type V = Unwrap<Promise<number>>;               // number

// 배열의 원소 타입
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<User[]>;                    // User

infer 는 "타입 매칭하면서 일부를 변수에 담아두기". 정규식의 캡처 그룹과 닮았습니다.

분배 조건부 타입 — 유니온이 자동 분기

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

type X = ToArray<string | number>;
// 의외: string[] | number[]   ← 유니온 각 멤버에 자동 분배
// 직관: (string | number)[]   ← 아님!

// 왜? 유니온 + 조건부 = 자동 분배 (벗기지 않게 하려면 [] 로 감싸기)
type ToArrayNo<T> = [T] extends [any] ? T[] : never;
type Y = ToArrayNo<string | number>;   // (string | number)[]

분배의 함정. "T extends U" 에서 T 가 유니온이면 자동으로 멤버별로 풀려서 적용됩니다. 대부분 유용하지만 가끔 의도와 다를 때 위 [T] extends [U] 트릭으로 막습니다.

Exclude · Extract — 분배의 응용

// 유틸리티 타입의 실제 정의 (단순)
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;

// 동작 (분배 덕분)
type A = MyExclude<"a" | "b" | "c", "b">;
// = ("a" extends "b" ? never : "a")    → "a"
// | ("b" extends "b" ? never : "b")    → never
// | ("c" extends "b" ? never : "c")    → "c"
// = "a" | "c"

실전 — API 응답 타입 안전하게 풀기

type ApiResponse<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };

// 성공만 골라 data 타입 뽑기
type SuccessData<R> = R extends { ok: true; data: infer D } ? D : never;

type UserResp = ApiResponse<{ id: number; name: string }>;
type UserData = SuccessData<UserResp>;
// = { id: number; name: string }

literal 좁히기 + 조건부

// 입력 타입에 따라 반환 타입이 달라지는 함수
function format<T extends number | string>(x: T): T extends number ? string : number {
  return (typeof x === "number" ? String(x) : Number(x)) as any;
}

const a = format(10);       // string
const b = format("10");     // number

이게 overload(3편) 의 더 강력한 대안. 입력-출력 관계를 한 줄로 표현.

실전 자주 만나는 조건부 타입 패턴

패턴역할
T extends Function ? ... : ...함수만 분기
T extends any[] ? ... : ...배열만 분기
T extends Promise<infer V> ? V : TPromise 풀기
F extends (...a: infer A) => any ? A : never함수 매개변수 튜플
T extends { type: U } ? T : neverdiscriminated 유니온 좁히기
T extends \`${U}${infer R}\` ? R : never템플릿 리터럴 타입 (5.0+)

너무 깊게 들어가지 마세요. 조건부 타입을 5-6 단계 중첩한 코드는 누구도 못 읽습니다. 라이브러리 코드라면 가치 있지만, 일반 앱 코드는 유틸리티 타입 조합 으로 끝내는 게 보통 정답.

13편 — 매핑 타입 (keyof + [K in T])

속성 단위로 타입을 변형하는 더 강력한 도구.

📚 쉽게 배우는 타입스크립트 교재
이전: 11편 유틸리티 · 현재: 12편 (중급) · 다음 → 13편 매핑 타입 · 진행: 12/20

© 2026 주나이테크(주) @JUNAITECH