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

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

이 글의 핵심

TypeScript 고급 패턴: 조건부 타입, 템플릿 리터럴 타입. 조건부 타입·infer 키워드.

들어가며

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

JavaScript에서 TypeScript로의 전환

“런타임 에러는 프로덕션에서 터진다”는 말이 있습니다. JavaScript로 개발하던 시절, 이 말을 뼈저리게 체감했습니다. 테스트는 다 통과했는데 배포 후 사용자가 이상한 데이터를 입력하면 앱이 터지는 거죠. 특히 팀 프로젝트에서 다른 사람이 작성한 함수의 반환 타입을 잘못 이해해서 버그가 발생하는 일이 잦았습니다. TypeScript를 도입한 후 가장 먼저 느낀 건 안심이었습니다. IDE가 자동완성을 정확히 해주고, 타입이 맞지 않으면 빨간 줄로 미리 알려줍니다. 리팩토링할 때도 “이 함수를 바꾸면 어디가 깨질까?” 걱정할 필요가 없어졌죠. 물론 처음엔 타입 정의 작성이 번거로웠지만, 몇 달 후 그 타입 정의 덕분에 버그를 미리 잡는 경험을 하고 나니 돌아갈 수 없었습니다.

1. 조건부 타입

기본 문법

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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

실전 예제

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 배열인지 확인
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

중첩 조건부 타입

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 키워드

개념

조건부 타입에서 타입을 추론합니다. 아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

함수 반환 타입 추출

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

함수 매개변수 타입 추출

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 타입 추출

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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. 템플릿 리터럴 타입

기본 사용

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
interface Events {
    onClick: () => void;
    onFocus: () => void;
    onBlur: () => void;
}

문자열 조작 유틸리티

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

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 엔드포인트

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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. 매핑 타입

기본 매핑

다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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>;

키 재매핑

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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;
// }

키 필터링

다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 타입 안전한 이벤트 시스템

다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 타입 안전한 상태 관리

다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: "hong@test.com" }
});

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

다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 키워드

고급 패턴 활용

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

다음 단계


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3