조건부 타입
조건부 타입은 타입스크립트에서 타입 관계를 기반으로 타입을 선택할 수 있게 해주는 강력한 기능입니다.
이를 통해 유연하고 표현력 있는 타입 정의가 가능해집니다.
조건부 타입의 기본 문법
조건부 타입의 기본 문법은 삼항 연산자와 유사합니다.
T extends U ? X : Y
이는 "T가 U에 할당 가능하면 X, 그렇지 않으면 Y"로 해석됩니다.
예시
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
제네릭과 조건부 타입의 결합
제네릭과 조건부 타입을 함께 사용하면 매우 유연한 타입 정의가 가능합니다.
type ArrayOrSingle<T> = T extends any[] ? T : T[];
function wrapInArray<T>(value: T): ArrayOrSingle<T> {
return Array.isArray(value) ? value : [value];
}
const result1 = wrapInArray(42); // number[]
const result2 = wrapInArray([1, 2, 3]); // number[]
infer 키워드 활용
infer
키워드를 사용하면 조건부 타입 내에서 타입을 추론하고 사용할 수 있습니다.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // number
여기서 infer R
은 함수의 반환 타입을 추론하여 R
에 할당합니다.
유니온 타입과 조건부 타입의 상호작용
조건부 타입은 유니온 타입에 대해 분배 법칙을 따릅니다.
type ToArray<T> = T extends any ? T[] : never;
type StrNumBool = string | number | boolean;
type StrNumBoolArray = ToArray<StrNumBool>; // string[] | number[] | boolean[]
이 분배 법칙은 복잡한 타입 변환을 간단하게 만들어줍니다.
내장 조건부 타입
타입스크립트는 여러 유용한 내장 조건부 타입을 제공합니다.
type A = Exclude<string | number | boolean, boolean>; // string | number
type B = Extract<string | number | boolean, boolean | number>; // number | boolean
type C = NonNullable<string | number | null | undefined>; // string | number
type D = ReturnType<() => void>; // void
이러한 내장 타입들의 구현을 살펴보면 조건부 타입의 강력함을 이해할 수 있습니다.
조건부 타입을 활용한 함수 오버로딩
조건부 타입을 사용하면 복잡한 함수 오버로딩을 단순화할 수 있습니다.
type Flatten<T> = T extends Array<infer U> ? U : T;
function flatten<T>(value: T): Flatten<T> {
return Array.isArray(value) ? value[0] : value;
}
const a = flatten([1, 2, 3]); // number
const b = flatten(42); // number
이 접근 방식은 API를 더 간결하고 유연하게 만들어줍니다.
조건부 타입과 매핑된 타입의 결합
조건부 타입과 매핑된 타입을 함께 사용하면 복잡한 타입 변환을 수행할 수 있습니다.
type NonNullableProperties<T> = {
[K in keyof T]: NonNullable<T[K]>;
};
interface User {
name: string | null;
age: number | undefined;
}
type NonNullableUser = NonNullableProperties<User>;
// { name: string; age: number; }
재귀적 조건부 타입
재귀적 조건부 타입을 사용하면 복잡한 중첩 구조를 다룰 수 있습니다.
type DeepNonNullable<T> = T extends object
? { [K in keyof T]: DeepNonNullable<T[K]> }
: NonNullable<T>;
type NestedObj = {
a: string | null;
b: { c: number | undefined, d: string | null }[];
};
type DeepNonNull = DeepNonNullable<NestedObj>;
// { a: string; b: { c: number, d: string }[] }
순환 참조 문제와 해결 전략
복잡한 조건부 타입에서는 순환 참조 문제가 발생할 수 있습니다.
type Circular<T> = T extends Array<infer U> ? Circular<U> : T;
// Error: Type alias 'Circular' circularly references itself.
이를 해결하기 위해 인터페이스를 사용하거나, 조건을 추가하여 순환을 방지할 수 있습니다.
type Circular<T> = T extends Array<infer U> ? (U extends Array<any> ? U : Circular<U>) : T;
설계 원칙과 Best Practices
-
단순성 유지 : 가능한 한 간단한 조건부 타입을 사용하세요. 복잡한 로직은 여러 단계로 나누어 구현하세요.
-
가독성 중시 : 복잡한 조건부 타입은 주석을 통해 설명하거나, 의미 있는 중간 타입 별칭을 사용하세요.
-
제네릭 활용 : 재사용 가능한 조건부 타입을 만들기 위해 제네릭을 적극 활용하세요.
-
분배 법칙 이해 : 유니온 타입에 대한 조건부 타입의 동작을 이해하고 활용하세요.
-
내장 타입 숙지 : 타입스크립트의 내장 조건부 타입을 잘 알고 활용하세요.
-
과도한 복잡성 주의 : 너무 복잡한 조건부 타입은 유지보수를 어렵게 만들 수 있습니다. 필요한 경우 런타임 로직으로 대체하는 것을 고려하세요.
-
테스트 작성 : 복잡한 조건부 타입은 단위 테스트를 통해 검증하세요.
-
성능 고려 : 매우 복잡한 조건부 타입은 컴파일 시간에 영향을 줄 수 있으므로 주의해서 사용하세요.
-
문서화 : 재사용 가능한 조건부 타입은 문서화하여 다른 개발자들이 쉽게 이해하고 사용할 수 있게 하세요.
-
실제 사용 사례 기반 : 실제 문제 해결을 위해 조건부 타입을 사용하고, 과도한 추상화는 피하세요.
조건부 타입은 타입스크립트에서 강력한 타입 조작 도구입니다.
이를 통해 복잡한 타입 로직을 표현하고, 유연하고 재사용 가능한 타입 정의를 만들 수 있습니다.