TypeScript 고급 패턴 | 조건부 타입, 템플릿 리터럴 타입

TypeScript 고급 패턴 | 조건부 타입, 템플릿 리터럴 타입

이 글의 핵심

TypeScript 고급 패턴에 대한 실전 가이드입니다. 조건부 타입, 템플릿 리터럴 타입 등을 예제와 함께 상세히 설명합니다.

들어가며

TypeScript의 고급 타입 시스템(조건부 타입, infer, 템플릿 리터럴 등)은 명찰을 규칙으로 조합해 API 모양을 컴파일 타임에 맞추는 데 쓰입니다. 런타임 비용 없이 “이런 문자열만 허용” 같은 제약을 걸 수 있습니다.


1. 조건부 타입

기본 문법

// T extends U ? X : Y
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;   // true
type B = IsString<number>;   // false
type C = IsString<"hello">;  // true

실전 예제

// 배열인지 확인
type IsArray<T> = T extends any[] ? true : false;

type D = IsArray<string[]>;  // true
type E = IsArray<number>;    // false

// 함수인지 확인
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

type F = IsFunction<() => void>;  // true
type G = IsFunction<string>;      // false

중첩 조건부 타입

type TypeName<T> = 
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T1 = TypeName<string>;    // "string"
type T2 = TypeName<42>;        // "number"
type T3 = TypeName<() => void>; // "function"

2. infer 키워드

개념

조건부 타입에서 타입을 추론합니다.

// 배열 요소 타입 추출
type ElementType<T> = T extends (infer U)[] ? U : never;

type H = ElementType<string[]>;  // string
type I = ElementType<number[]>;  // number
type J = ElementType<string>;    // never

함수 반환 타입 추출

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
    return { id: "U001", name: "홍길동" };
}

type User = MyReturnType<typeof getUser>;
// { id: string; name: string; }

function getNumber(): number {
    return 42;
}

type Num = MyReturnType<typeof getNumber>;  // number

함수 매개변수 타입 추출

type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

function createUser(name: string, age: number, email: string) {
    return { name, age, email };
}

type Params = MyParameters<typeof createUser>;
// [string, number, string]

// 첫 번째 매개변수 타입
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type First = FirstParam<typeof createUser>;  // string

Promise 타입 추출

type Awaited<T> = T extends Promise<infer U> ? U : T;

type K = Awaited<Promise<string>>;  // string
type L = Awaited<Promise<number>>;  // number
type M = Awaited<string>;           // string

3. 템플릿 리터럴 타입

기본 사용

type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

interface Events {
    onClick: () => void;
    onFocus: () => void;
    onBlur: () => void;
}

문자열 조작 유틸리티

type Greeting = "hello world";

type Upper = Uppercase<Greeting>;      // "HELLO WORLD"
type Lower = Lowercase<Greeting>;      // "hello world"
type Cap = Capitalize<Greeting>;       // "Hello world"
type Uncap = Uncapitalize<Greeting>;   // "hello world"

API 엔드포인트

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Resource = "users" | "posts" | "comments";

type ApiEndpoint = `/${Resource}`;
// "/users" | "/posts" | "/comments"

type ApiRoute = `${HttpMethod} ${ApiEndpoint}`;
// "GET /users" | "POST /users" | ...

type IdEndpoint = `${ApiEndpoint}/${string}`;
// "/users/:id" | "/posts/:id" | ...

4. 매핑 타입

기본 매핑

type MyReadonly<T> = {
    readonly [K in keyof T]: T[K];
};

type MyPartial<T> = {
    [K in keyof T]?: T[K];
};

type MyRequired<T> = {
    [K in keyof T]-?: T[K];
};

interface User {
    name: string;
    age?: number;
}

type ReadonlyUser = MyReadonly<User>;
type PartialUser = MyPartial<User>;
type RequiredUser = MyRequired<User>;

키 재매핑

type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User {
    name: string;
    age: number;
}

type UserGetters = Getters<User>;
// {
//     getName: () => string;
//     getAge: () => number;
// }

키 필터링

type PickByType<T, U> = {
    [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface User {
    id: string;
    name: string;
    age: number;
    isActive: boolean;
}

type StringProps = PickByType<User, string>;
// { id: string; name: string; }

type NumberProps = PickByType<User, number>;
// { age: number; }

5. 실전 예제

예제 1: 타입 안전한 이벤트 시스템

type EventMap = {
    "user:login": { userId: string; timestamp: Date };
    "user:logout": { userId: string };
    "post:create": { postId: string; title: string };
    "post:delete": { postId: string };
};

class EventEmitter<T extends Record<string, any>> {
    private listeners: {
        [K in keyof T]?: Array<(data: T[K]) => void>;
    } = {};
    
    on<K extends keyof T>(event: K, listener: (data: T[K]) => void) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event]!.push(listener);
    }
    
    emit<K extends keyof T>(event: K, data: T[K]) {
        const listeners = this.listeners[event];
        if (listeners) {
            listeners.forEach(listener => listener(data));
        }
    }
}

const emitter = new EventEmitter<EventMap>();

emitter.on("user:login", (data) => {
    console.log(`사용자 로그인: ${data.userId}`);
});

emitter.emit("user:login", {
    userId: "U001",
    timestamp: new Date()
});

예제 2: 타입 안전한 상태 관리

type State = {
    user: { name: string; email: string } | null;
    posts: Array<{ id: string; title: string }>;
    loading: boolean;
};

type Action<T extends keyof State> = {
    type: `SET_${Uppercase<string & T>}`;
    payload: State[T];
};

type Actions = {
    [K in keyof State]: Action<K>;
}[keyof State];

function reducer(state: State, action: Actions): State {
    switch (action.type) {
        case "SET_USER":
            return { ...state, user: action.payload };
        case "SET_POSTS":
            return { ...state, posts: action.payload };
        case "SET_LOADING":
            return { ...state, loading: action.payload };
        default:
            return state;
    }
}

const initialState: State = {
    user: null,
    posts: [],
    loading: false
};

const newState = reducer(initialState, {
    type: "SET_USER",
    payload: { name: "홍길동", email: "[email protected]" }
});

예제 3: 타입 안전한 쿼리 빌더

type QueryOperator = "eq" | "ne" | "gt" | "lt" | "gte" | "lte";

type Query<T> = {
    [K in keyof T]?: {
        [O in QueryOperator]?: T[K];
    };
};

interface User {
    id: string;
    name: string;
    age: number;
    email: string;
}

function find<T>(query: Query<T>): T[] {
    return [];
}

const users = find<User>({
    age: { gte: 20, lt: 30 },
    name: { eq: "홍길동" }
});

6. 고급 유틸리티 타입

DeepPartial

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

interface User {
    id: string;
    profile: {
        name: string;
        address: {
            city: string;
            zipCode: string;
        };
    };
}

const update: DeepPartial<User> = {
    profile: {
        address: {
            city: "서울"
        }
    }
};

DeepReadonly

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

const user: DeepReadonly<User> = {
    id: "U001",
    profile: {
        name: "홍길동",
        address: {
            city: "서울",
            zipCode: "12345"
        }
    }
};

정리

핵심 요약

  1. 조건부 타입: T extends U ? X : Y
  2. infer: 타입 추론 (함수, Promise 등)
  3. 템플릿 리터럴: 문자열 타입 조합
  4. 매핑 타입: 타입 변환
  5. 키 재매핑: as 키워드

고급 패턴 활용

  • 타입 안전한 이벤트 시스템
  • 상태 관리 타입
  • 쿼리 빌더 타입
  • 유틸리티 타입 확장

다음 단계

  • TypeScript 실전 프로젝트
  • TypeScript 유틸리티 타입
  • TypeScript 제네릭

관련 글

  • Kotlin 고급 기능 | DSL, 리플렉션, 애노테이션
  • Python 데코레이터 | @decorator 완벽 정리
  • TypeScript 시작하기 | 설치, 설정, 기본 문법
  • TypeScript 고급 타입 | Union, Intersection, Literal 타입
  • TypeScript 인터페이스 | Interface 완벽 가이드