[TS] 조건부 타입 (Conditional Types)

01
조건부 타입 이란?
조건부 타입은 어떤 조건이 참인지 거짓인지에 따라 다른 타입을 반환하는 기능입니다.
type A = number extends string ? number : string;
여기서 number extends string은 거짓이니까, A의 타입은 string이 됩니다
💡 예제: 객체 타입 비교
객체 타입에서도 동일한 원리가 적용됩니다.
type ObjA = { a: number };
type ObjB = { a: number; b: number };
type B = ObjB extends ObjA ? number : string;
ObjB는 ObjA를 포함하는 상위 타입이므로, B의 타입은 number가 됩니다.
02
제네릭 조건부 타입
제네릭을 활용하면 타입을 유동적으로 설정할 수 있습니다!
type StringNumberSwitch<T> = T extends number ? string : number;
let varA: StringNumberSwitch<number>; // string
let varB: StringNumberSwitch<string>; // number
여기서 T가 numbe r면 string을 반환하고, 아니면 number를 반환합니다.
제네릭 조건부 타입을 활용하면 다양한 상황에서 유용하게 사용할 수 있습니다.
💡 예제: 공백을 제거하는 함수
다음은 문자열의 공백을 제거하는 함수입니다.
function removeSpaces(text: string) {
return text.replaceAll(" ", "");
}
하지만 이 함수는 undefined나 null이 전달될 경우 오류가 발생할 수 있습니다.
이를 해결하기 위해 조건부 타입을 활용할 수 있습니다.
function removeSpaces<T>(text: T): T extends string ? string : undefined {
if (typeof text === "string") {
return text.replaceAll(" ", "") as any; // ❌ 타입 단언 사용
} else {
return undefined as any;
}
}
하지만 any를 쓰면 타입 검사가 제대로 안 되니까, 함수 오버로딩을 사용해야 하죠.
function removeSpaces<T>(text: T): T extends string ? string : undefined;
function removeSpaces(text: any) {
if (typeof text === "string") {
return text.replaceAll(" ", "");
} else {
return undefined;
}
}
text가 string이면 공백을 제거한 값을 반환하고, 그렇지 않으면 undefined를 반환하도록 설계되었습니다.
03
분산적인 조건부 타입
유니온 타입을 조건부 타입과 함께 사용하면 자동으로 분리됩니다!
type StringNumberSwitch<T> = T extends number ? string : number;
let c: StringNumberSwitch<number | string>; // string | number
number | string이 각각 따로 평가된 후 합쳐져서 string | number 타입이 됩니다.
이러한 특성을 활용하면 특정 타입을 제거하는 기능도 구현할 수 있습니다.
💡 예제: 특정 타입 제거하기
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<number | string | boolean, string>; // number | boolean
string 타입이 제거되고 number | boolean 타입만 남게 됩니다.
04
infer로 타입 추론하기
infer는 조건부 타입 내부에서 특정 타입을 자동으로 추론할 수 있습니다!
type ReturnType<T> = T extends () => infer R ? R : never;
type FuncA = () => string;
type A = ReturnType<FuncA>; // string
infer R은 함수의 반환 타입을 자동으로 추론하여 A의 타입을 string으로 변환합니다.
💡 예제: Promise 결괏값 추출하기
다음은 Promise 타입에서 결괏값의 타입을 추출하는 예제입니다.
type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;
type Result = PromiseUnpack<Promise<number>>; // number
Promise <number> 타입에서 number만 추출하여 Result의 타입이 number가 됩니다.
05
추가 예제
💡 예제: IsProductKey 타입 구현하기
Product의 key인지 확인하는 타입을 만들기
// Product 인터페이스 정의
interface Product {
id: number;
name: string;
price: number;
seller: {
id: number;
name: string;
company: string;
};
}
// Product의 key인지 확인하는 타입
type IsProductKey<T> = T extends keyof Product ? true : false;
// ✅ 테스트
type Test1 = IsProductKey<"id">; // true
type Test2 = IsProductKey<"name">; // true
type Test3 = IsProductKey<"seller">; // true
type Test4 = IsProductKey<"company">; // false (Product의 직접적인 key가 아님)
type Test5 = IsProductKey<"price">; // true
type Test6 = IsProductKey<"randomKey">; // false
✅ id, name, price, seller는 Product의 키이므로 true
❌ company는 seller의 내부 속성이므로 false
💡 예제: 배열 타입의 요소를 추출하는 InferArrayType <T> 타입 구현하기
배열 T가 Array<infer R> 또는 (infer R)[]의 형태인지 확인하고, 그렇다면 R(요소 타입)을 반환한다.
그렇지 않다면 never을 반환한다.
// 배열의 요소 타입을 추출하는 타입
type InferArrayType<T> = T extends Array<infer R> ? R : never;
/* 또는 같은 의미로 작성 가능 */
type InferArrayType<T> = T extends (infer R)[] ? R : never;
✅ 테스트
import { Expect, Equal } from "@type-challenges/utils";
const arr1 = [1, 2, 3];
const arr2 = ["hello", "myname", "winterlood"];
const arr3 = [1, true, "hi"];
// 기대 결과와 비교하여 타입 체크
type TestCases = [
Expect<Equal<InferArrayType<typeof arr1>, number>>, // ✅ number
Expect<Equal<InferArrayType<typeof arr2>, string>>, // ✅ string
Expect<Equal<InferArrayType<typeof arr3>, number | string | boolean>> // ✅ 요소 타입이 여러 개면 유니온 타입으로 추출
];
💡 예제: Extract<T, U> 타입 구현하기
T에서 U에 해당하는 타입만 추출하는 타입을 구하기
// Extract<T, U>: T에서 U에 해당하는 타입만 남기는 타입
type Extract<T, U> = T extends U ? T : never;
// ✅ 테스트
type E1 = Extract<"apple" | "banana" | "cherry", "banana" | "orange">;
// 결과: "banana" (교집합)
type E2 = Extract<number | string | boolean, string | boolean>;
// 결과: string | boolean (number는 제거됨)
type E3 = Extract<1 | 2 | 3, 2 | 3 | 4>;
// 결과: 2 | 3 (4는 T에 없어서 제외)
type E4 = Extract<"a" | "b" | "c", "x" | "y">;
// 결과: never (공통되는 타입이 없음)
type E5 = Extract<"frontend" | "backend", "frontend">;
// 결과: "frontend" (일치하는 값만 남음)
✅ Extract<T, U>는 T에서 U에 포함된 타입만 남기는 역할을 하죠!
❌ Exclude<T, U>와 반대로 동작한다는 점을 기억하면 좋습니다!
번외) Extract 와 Exclude
타입스크립트에서 Extract<T, U>와 Exclude <T, U>는 유니온 타입에서 특정 타입을 남기거나 제거하는 역할을 합니다.
이 둘은 완전히 반대되는 동작을 합니다.
✅ Extract<T, U> → "U에 포함된 것만 남기기"
✅ Exclude<T, U> → "U에 포함된 것 제거하기"
💡 예제: Extract<T, U>
T의 각 요소가 U에 포함되면 유지하고, 아니면 제거하는 것!
//정의
type Extract<T, U> = T extends U ? T : never;
//동작 과정
type A = Extract<"a" | "b" | "c", "a" | "c">;
// 결과: "a" | "c"
💡 예제: Exclude<T, U>
T의 각 요소가 U에 포함되면 제거하고, 아니면 유지하는 것!
//정의
type Exclude<T, U> = T extends U ? never : T;
//동작 과정
type B = Exclude<"a" | "b" | "c", "a" | "c">;
// 결과: "b"
06
마무리
기능 | 설명 |
조건부 타입 | extends와 ? :을 이용해 조건에 따라 다른 타입을 반환합니다. |
제네릭 조건부 타입 | 입력 타입에 따라 유연하게 동작할 수 있도록 만듭니다. |
분산적인 조건부 타입 | 유니온 타입을 개별적으로 평가한 후 다시 합쳐줍니다. |
infer를 활용한 타입 추론 | 특정 타입을 자동으로 추출할 수 있습니다. |
이제 조건부 타입을 이해하고 활용할 수 있을 것입니다! TypeScript 타입 시스템을 적극적으로 활용해 보세요. 😆🔥