개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

조건부 타입 — 타입 시스템의 if문

2025년 11월 26일
TypeScriptConditional Types타입infer

TypeScript의 타입 시스템은 값을 다루지 않지만, 타입 단계에서도 if문처럼 조건에 따라 다른 타입을 선택할 수 있습니다. 이것이 조건부 타입(Conditional Types)입니다.

기본 문법

T extends U ? X : Y

T가 U에 할당 가능하면 X, 아니면 Y를 반환합니다. 조건부 타입에서 extends는 상속이 아닌 타입 호환성 검사입니다.

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

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true — "hello"는 string에 할당 가능

내장 유틸리티 타입 구현

Exclude, Extract, NonNullable 같은 유틸리티 타입이 조건부 타입으로 구현되어 있습니다.

// Exclude: U에 해당하는 타입 제거
type MyExclude<T, U> = T extends U ? never : T;

type Result = MyExclude<"a" | "b" | "c", "b">;
// "a" | "c"

// NonNullable: null과 undefined 제거
type MyNonNullable<T> = T extends null | undefined ? never : T;

type Clean = MyNonNullable<string | null | undefined>;
// string

분배 법칙

타입 매개변수가 유니온 타입이면, 조건부 타입이 각 멤버에 분배 적용됩니다.

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

type Result = ToArray<string | number>;
// string[] | number[] (분배 적용)
// — string에 적용 → string[]
// — number에 적용 → number[]

분배 법칙을 막으려면 타입을 튜플로 감쌉니다.

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

type Result = ToArrayNoDistribute<string | number>;
// (string | number)[] (분배 안 됨)

분배 법칙(Distributive Conditional Types): 벗겨진(naked) 타입 매개변수에 조건부 타입을 적용하면 유니온의 각 멤버에 개별 적용됩니다.

infer

조건부 타입 안에서 타입의 일부를 변수처럼 캡처하는 키워드입니다.

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

function getUser() {
  return { id: 1, name: "철수" };
}

type UserReturn = ReturnType<typeof getUser>;
// { id: number; name: string }

infer R은 "함수의 반환 타입을 R로 캡처해 사용하겠다"는 의미입니다.

다양한 infer 활용

// 함수 매개변수 타입 추출
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

function greet(name: string, age: number) {}
type FirstArg = FirstParam<typeof greet>; // string

// Promise가 감싼 타입 추출
type Awaited<T> = T extends Promise<infer V> ? V : T;

type Resolved = Awaited<Promise<string>>; // string
type Direct   = Awaited<number>;          // number

// 배열 원소 타입 추출
type ElementType<T> = T extends (infer E)[] ? E : never;

type Str = ElementType<string[]>; // string
type Num = ElementType<number[]>; // number

재귀 조건부 타입

자기 자신을 참조하는 재귀 구조도 만들 수 있습니다.

// 중첩 배열을 펼쳐서 원소 타입 추출
type FlatArray<T> = T extends (infer U)[]
  ? U extends any[]
    ? FlatArray<U>
    : U
  : T;

type Flat = FlatArray<number[][][]>; // number

실전 예시 — 함수 오버로드 대체

// 입력 타입에 따라 반환 타입이 달라지는 함수
type WrapResult<T> = T extends string
  ? { type: "string"; value: T }
  : T extends number
  ? { type: "number"; value: T }
  : { type: "other"; value: T };

function wrap<T>(value: T): WrapResult<T> {
  if (typeof value === "string") {
    return { type: "string", value } as WrapResult<T>;
  }
  if (typeof value === "number") {
    return { type: "number", value } as WrapResult<T>;
  }
  return { type: "other", value } as WrapResult<T>;
}

const s = wrap("hello");  // { type: "string"; value: string }
const n = wrap(42);       // { type: "number"; value: number }

조건부 타입은 TypeScript 타입 시스템에서 가장 표현력이 높은 기능입니다. infer와 결합하면 함수 시그니처, Promise, 배열 등 복잡한 타입 구조에서 원하는 부분만 정밀하게 추출할 수 있습니다.

← 이전 글맵드 타입 — 기존 타입의 모든 프로퍼티를 변환하기
다음 글 →데코레이터 — 클래스와 메서드에 기능을 추가하는 문법