제네릭 심화에서는 조건부 타입의 분배 동작, 재귀 타입, 그리고 타입 레벨에서 연산을 수행하는 방법을 다룹니다.

조건부 분배 심화

TYPESCRIPT
// 유니온 + 조건부 타입 = 분배
type Boxed<T> = T extends any ? { value: T } : never;

type Result = Boxed<string | number>;
// { value: string } | { value: number }
// ⚠️ { value: string | number }가 아님!

분배 제어하기

TYPESCRIPT
// 분배 방지: 대괄호로 감싸기
type BoxedNonDist<T> = [T] extends [any] ? { value: T } : never;

type Result2 = BoxedNonDist<string | number>;
// { value: string | number } — 분배 없이 통째로 처리

// 분배가 필요한 경우와 아닌 경우를 구분하기
type IsUnion<T, C = T> =
  T extends C
    ? [C] extends [T]
      ? false
      : true
    : never;

type A = IsUnion<string>;          // false
type B = IsUnion<string | number>; // true

재귀 타입(Recursive Types)

Deep Partial

TYPESCRIPT
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      user: string;
      password: string;
    };
  };
  logging: {
    level: string;
  };
}

type PartialConfig = DeepPartial<Config>;
// database?.credentials?.user? — 모든 레벨이 선택적

깊은 키 경로 추출

TYPESCRIPT
type PathKeys<T, Prefix extends string = ''> = T extends object
  ? {
      [K in keyof T & string]:
        | `${Prefix}${K}`
        | PathKeys<T[K], `${Prefix}${K}.`>;
    }[keyof T & string]
  : never;

interface User {
  name: string;
  address: {
    city: string;
    zip: number;
  };
}

type UserPaths = PathKeys<User>;
// 'name' | 'address' | 'address.city' | 'address.zip'

Flatten (중첩 배열 펼치기)

TYPESCRIPT
type Flatten<T> = T extends (infer U)[]
  ? Flatten<U>
  : T;

type A = Flatten<number[]>;         // number
type B = Flatten<number[][]>;       // number
type C = Flatten<number[][][]>;     // number
type D = Flatten<string>;           // string

타입 레벨 산술

TypeScript의 타입 시스템에서 숫자 연산을 구현하는 트릭입니다. 튜플의 길이를 이용합니다.

TYPESCRIPT
// 튜플의 길이로 숫자를 표현
type Length<T extends any[]> = T['length'];

type Zero = Length<[]>;        // 0
type Three = Length<[1, 2, 3]>; // 3

// N 길이의 튜플 생성
type BuildTuple<N extends number, T extends any[] = []> =
  T['length'] extends N ? T : BuildTuple<N, [...T, unknown]>;

type FiveTuple = BuildTuple<5>; // [unknown, unknown, unknown, unknown, unknown]

// 덧셈
type Add<A extends number, B extends number> =
  Length<[...BuildTuple<A>, ...BuildTuple<B>]>;

type Sum = Add<3, 4>; // 7

// 뺄셈
type Subtract<A extends number, B extends number> =
  BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest]
    ? Rest['length']
    : never;

type Diff = Subtract<7, 3>; // 4

공부하다 보니 TypeScript 타입 시스템이 사실상 프로그래밍 언어 수준이더라고요. 물론 실무에서 타입 레벨 산술을 쓸 일은 드물지만, 타입 챌린지에서 자주 나옵니다.

제네릭 함수의 타입 추론 개선

TYPESCRIPT
// ❌ T가 너무 넓게 추론됨
function createPair<T>(a: T, b: T): [T, T] {
  return [a, b];
}
// createPair(1, 'hello'); // Error — T가 number | string으로 추론 안 됨

// ✅ 각각 다른 타입 매개변수
function createPair2<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}
const pair = createPair2(1, 'hello'); // [number, string]

NoInfer (TS 5.4+)

TYPESCRIPT
// NoInfer로 특정 위치에서 타입 추론을 막기
function createFSM<S extends string>(
  initial: NoInfer<S>,
  states: S[]
) {
  return { current: initial, states };
}

// states에서 S가 추론되고, initial은 추론에 참여하지 않음
createFSM('idle', ['idle', 'loading', 'done']); // OK
// createFSM('invalid', ['idle', 'loading', 'done']); // ❌ Error

제네릭 제약과 조건부 타입 조합

TYPESCRIPT
// 값이 배열이면 요소 타입을, 아니면 그대로 반환
type Unwrap<T> = T extends readonly (infer U)[] ? U : T;

function first<T>(value: T): Unwrap<T> {
  if (Array.isArray(value)) {
    return value[0];
  }
  return value as Unwrap<T>;
}

const a = first([1, 2, 3]);  // number
const b = first('hello');     // string

정리

  • 유니온에 조건부 타입을 적용하면 각 멤버에 분배된다 (방지: [T] extends [U])
  • 재귀 타입으로 DeepPartial, Flatten 등 깊은 구조의 변환이 가능하다
  • 타입 레벨 산술은 튜플 길이를 이용해 덧셈, 뺄셈을 구현한다
  • NoInfer로 특정 위치에서 타입 추론을 막을 수 있다 (TS 5.4+)
  • 실무보다는 타입 챌린지와 라이브러리 개발에서 주로 사용되는 패턴이다
댓글 로딩 중...