icon안동민 개발노트

조건부 타입과 제네릭


 조건부 타입과 제네릭의 결합은 타입스크립트에서 매우 강력한 타입 연산을 가능하게 합니다.

 이를 통해 복잡한 타입 로직을 구현하고 동적인 타입 결정을 할 수 있습니다.

기본 문법과 고급 타입 연산

 조건부 타입의 기본 문법은 다음과 같습니다.

type ConditionalType<T> = T extends SomeType ? TrueType : FalseType;

 이를 제네릭과 결합하여 사용하면,

type IsArray<T> = T extends any[] ? true : false;
 
type CheckArray = IsArray<number[]>;  // true
type CheckNumber = IsArray<number>;   // false

동적 타입 결정 메커니즘

 제네릭 타입 매개변수를 조건부 타입의 조건으로 사용하면 동적으로 타입을 결정할 수 있습니다.

type ElementType<T> = T extends (infer U)[] ? U : T;
 
type StringArrayElement = ElementType<string[]>;  // string
type NumberElement = ElementType<number>;         // 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

분배 조건부 타입

 분배 조건부 타입은 유니온 타입에 대해 자동으로 분배됩니다.

type ToArray<T> = T extends any ? T[] : never;
 
type StringOrNumberArray = ToArray<string | number>;  // string[] | number[]

타입 필터링과 유틸리티 타입 구현

 조건부 타입을 사용하여 타입을 필터링하고 유틸리티 타입을 구현할 수 있습니다.

type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
 
type T0 = Exclude<"a" | "b" | "c", "a">;  // "b" | "c"
type T1 = Extract<"a" | "b" | "c", "a" | "f">;  // "a"

함수 오버로딩 시뮬레이션

 조건부 타입과 제네릭을 사용하여 함수 오버로딩을 시뮬레이션할 수 있습니다.

type Overloaded = {
    (x: string): number;
    (x: number): string;
};
 
function overloaded<T extends string | number>(
    x: T
): T extends string ? number : string {
    return (typeof x === "string" ? x.length : x.toString()) as any;
}
 
const result1 = overloaded("hello");  // number
const result2 = overloaded(42);       // string

 이 방식의 장점은 단일 함수 정의로 다양한 입력 타입을 처리할 수 있다는 것입니다.

 하지만 타입 추론이 복잡해질 수 있다는 단점이 있습니다.

재귀적 조건부 타입

 재귀적 조건부 타입을 사용하여 복잡한 타입 변환을 수행할 수 있습니다.

type DeepReadonly<T> = {
    readonly [P in keyof T]: T[P] extends object
        ? DeepReadonly<T[P]>
        : T[P];
};
 
interface NestedObject {
    a: number;
    b: {
        c: string;
        d: {
            e: boolean;
        };
    };
}
 
type ReadonlyNestedObject = DeepReadonly<NestedObject>;

고급 타입 추론 패턴

 조건부 타입과 제네릭을 결합하여 고급 타입 추론 패턴을 구현할 수 있습니다.

type FunctionProperties<T> = {
    [K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
 
type NonFunctionProperties<T> = {
    [K in keyof T]: T[K] extends Function ? never : K
}[keyof T];
 
interface Mixed {
    name: string;
    age: number;
    greet(): void;
}
 
type FuncProps = FunctionProperties<Mixed>;  // "greet"
type NonFuncProps = NonFunctionProperties<Mixed>;  // "name" | "age"

성능 최적화

 복잡한 조건부 타입과 제네릭 연산은 컴파일 시간을 증가시킬 수 있습니다.

 최적화하기 위한 전략

  1. 과도하게 복잡한 타입 연산을 피합니다.
  2. 타입 별칭을 사용하여 중간 결과를 캐시합니다.
  3. 타입 연산을 더 작은 단위로 분할합니다.
// 비효율적인 방식
type Inefficient<T> = T extends string
    ? string
    : T extends number
    ? number
    : T extends boolean
    ? boolean
    : object;
 
// 최적화된 방식
type IsString<T> = T extends string ? string : never;
type IsNumber<T> = T extends number ? number : never;
type IsBoolean<T> = T extends boolean ? boolean : never;
 
type Efficient<T> = IsString<T> | IsNumber<T> | IsBoolean<T> | object;

설계 원칙과 Best Practices

  1. 단순성 유지 : 가능한 한 간단한 조건부 타입을 사용하세요. 복잡한 로직은 여러 단계로 나누어 구현하세요.
  2. 가독성 중시 : 복잡한 조건부 타입은 주석을 통해 설명하거나, 의미 있는 중간 타입 별칭을 사용하세요.
  3. 제네릭 활용 : 재사용 가능한 조건부 타입을 만들기 위해 제네릭을 적극 활용하세요.
  4. 분배 법칙 이해 : 유니온 타입에 대한 조건부 타입의 동작을 이해하고 활용하세요.
  5. infer 키워드 활용 : 복잡한 타입 추론에는 infer 키워드를 사용하세요.
  6. 재귀적 타입 주의 : 재귀적 조건부 타입 사용 시 무한 재귀에 빠지지 않도록 주의하세요.
  7. 테스트 작성 : 복잡한 조건부 타입은 단위 테스트를 통해 검증하세요.
  8. 성능 고려 : 매우 복잡한 조건부 타입은 컴파일 시간에 영향을 줄 수 있으므로 주의해서 사용하세요.
  9. 문서화 : 재사용 가능한 조건부 타입은 문서화하여 다른 개발자들이 쉽게 이해하고 사용할 수 있게 하세요.
  10. 실제 사용 사례 기반 : 실제 문제 해결을 위해 조건부 타입과 제네릭을 사용하고, 과도한 추상화는 피하세요.

 조건부 타입과 제네릭의 결합은 타입스크립트에서 매우 강력한 타입 조작 도구입니다.

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