icon안동민 개발노트

조건부 타입


 조건부 타입은 타입스크립트에서 타입 관계를 기반으로 타입을 선택할 수 있게 해주는 강력한 기능입니다.

 이를 통해 유연하고 표현력 있는 타입 정의가 가능해집니다.

조건부 타입의 기본 문법

 조건부 타입의 기본 문법은 삼항 연산자와 유사합니다.

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

  1.  단순성 유지 : 가능한 한 간단한 조건부 타입을 사용하세요. 복잡한 로직은 여러 단계로 나누어 구현하세요.

  2.  가독성 중시 : 복잡한 조건부 타입은 주석을 통해 설명하거나, 의미 있는 중간 타입 별칭을 사용하세요.

  3.  제네릭 활용 : 재사용 가능한 조건부 타입을 만들기 위해 제네릭을 적극 활용하세요.

  4.  분배 법칙 이해 : 유니온 타입에 대한 조건부 타입의 동작을 이해하고 활용하세요.

  5.  내장 타입 숙지 : 타입스크립트의 내장 조건부 타입을 잘 알고 활용하세요.

  6.  과도한 복잡성 주의 : 너무 복잡한 조건부 타입은 유지보수를 어렵게 만들 수 있습니다. 필요한 경우 런타임 로직으로 대체하는 것을 고려하세요.

  7.  테스트 작성 : 복잡한 조건부 타입은 단위 테스트를 통해 검증하세요.

  8.  성능 고려 : 매우 복잡한 조건부 타입은 컴파일 시간에 영향을 줄 수 있으므로 주의해서 사용하세요.

  9.  문서화 : 재사용 가능한 조건부 타입은 문서화하여 다른 개발자들이 쉽게 이해하고 사용할 수 있게 하세요.

  10.  실제 사용 사례 기반 : 실제 문제 해결을 위해 조건부 타입을 사용하고, 과도한 추상화는 피하세요.

 조건부 타입은 타입스크립트에서 강력한 타입 조작 도구입니다.

 이를 통해 복잡한 타입 로직을 표현하고, 유연하고 재사용 가능한 타입 정의를 만들 수 있습니다.