📑 목차
타입스크립트의 조건부 타입은 마치 프로그래밍의 삼항 연산자처럼, 조건에 따라 타입을 결정할 수 있게 해주는 강력한 문법이다. 제네릭과 결합했을 때 그 진가를 발휘하며, 유틸리티 타입을 만드는 핵심 원리이기도 하다.
이번 포스팅에서는 조건부 타입의 기초부터 infer 키워드까지 핵심 내용을 정리해 본다.
1. 조건부 타입 기초
기본 문법은 삼항 연산자와 매우 유사하다. extends 키워드를 사용하여 조건식을 만든다 .
// T가 U의 서브 타입이면 X, 아니면 Y
T extends U ? X : Y
간단한 예제를 보자.
type A = number extends string ? number : string;
number는 string의 서브 타입이 아니므로 조건식은 거짓이 되고, 결과적으로 type A는 string이 된다 .
제네릭과 함께 사용하기
조건부 타입은 제네릭과 함께 쓸 때 훨씬 유용하다.
type StringNumberSwitch<T> = T extends number ? string : number;
let varA: StringNumberSwitch<number>; // string (참)
let varB: StringNumberSwitch<string>; // number (거짓)
실전 예제: removeSpaces 함수
공백을 제거하는 함수를 만들 때, 입력값이 string이면 반환값도 string, undefined면 반환값도 undefined가 되도록 타입을 정교하게 만들 수 있다 .
함수 오버로딩 시그니처와 조건부 타입을 결합하면 구현부의 타입을 안전하게 지키면서도 호출자에게 정확한 타입을 제공할 수 있다 .
// 함수 오버로딩 시그니처 + 조건부 타입 활용
function removeSpaces<T>(text: T): T extends string ? string : undefined;
function removeSpaces(text: any) {
if (typeof text === "string") {
return text.replaceAll(" ", "");
} else {
return undefined;
}
}
let result = removeSpaces("hi im winterlood"); // string 타입
let result2 = removeSpaces(undefined); // undefined 타입
2. 분산적인 조건부 타입 (Distributive Conditional Types)
만약 조건부 타입의 제네릭(T)에 유니온 타입을 할당하면 어떻게 될까?
type StringNumberSwitch<T> = T extends number ? string : number;
let c: StringNumberSwitch<number | string>;
// 결과: string | number
결과가 number 하나로 퉁쳐지는 것이 아니라 string | number가 되었다. 그 이유는 분산적인 조건부 타입이 동작했기 때문이다 .
동작 원리
제네릭에 유니온 타입이 들어오면, 타입스크립트는 각 타입을 분리해서 개별적으로 연산을 수행한 뒤 다시 합친다 .
- 분리:
StringNumberSwitch<number>|StringNumberSwitch<string> - 계산: (
numberextendsnumber?string: ...) | (stringextendsnumber? ... :number) - 결과:
string|number
활용: Exclude 타입 구현하기
이 특징을 이용하면 특정 타입을 제거하는 Exclude 유틸리티 타입을 직접 구현할 수 있다 .
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<number | string | boolean, string>;
동작 과정 :
numberextendsstring? ❌ -> numberstringextendsstring? ✅ -> neverbooleanextendsstring? ❌ -> boolean
결과: number | never | boolean
최종: number | boolean (유니온에서 never는 공집합이므로 사라진다 )
3. infer 키워드 (타입 추론)
infer는 조건부 타입 내에서 특정 타입을 추론해서 가져올 때 사용하는 키워드다. "이 위치에 오는 타입을 R이라고 부르고, 나중에 써먹자!"라는 의미다.
함수 반환 타입 추출하기 (ReturnType)
type ReturnType<T> = T extends () => infer R ? R : never;
type FuncA = () => string;
type FuncB = () => number;
type A = ReturnType<FuncA>; // string
type B = ReturnType<FuncB>; // number
해석: T가 함수 타입이라면, 그 함수의 반환 타입을 R이라고 추론하고, 결과로 R을 반환해라. (아니면 never) .
Promise 내부 타입 추출하기
type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;
type PromiseA = PromiseUnpack<Promise<number>>; // number
type PromiseB = PromiseUnpack<Promise<string>>; // string
해석: T가 Promise 타입이라면, 그 안에 들어있는 타입을 R이라고 추론하고 꺼내줘라.
요약
- 조건부 타입:
T extends U ? X : Y형태로 조건에 따라 타입을 결정한다. - 분산 조건부 타입: 제네릭에 유니온 타입을 넘기면 각 타입에 대해 개별적으로 연산이 수행된다. (
Exclude등의 원리) - infer: 조건부 타입 안에서 특정 부분의 타입을 추론하여 변수처럼 가져다 쓸 수 있다. (
ReturnType등의 원리)
