본문으로 건너뛰기
Previous
Next
Advanced TypeScript | Conditional Types· Template Literals

Advanced TypeScript | Conditional Types· Template Literals

Advanced TypeScript | Conditional Types· Template Literals

이 글의 핵심

Advanced TypeScript: conditional types, infer, template literals, mapped types, key remapping—type-safe events, reducers, and query objects.

Introduction

TypeScript’s advanced type system lets you write precise, type-safe code that scales with your domain.

1. Conditional types

Basic syntax

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

Practical checks

// Is it an array?
type IsArray<T> = T extends any[] ? true : false;
type D = IsArray<string[]>;  // true
type E = IsArray<number>;    // false
// Is it a function?
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
type F = IsFunction<() => void>;  // true
type G = IsFunction<string>;      // false

Nested conditionals

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. The infer keyword

Concept

infer introduces a type variable inside a conditional type.

// Element type of an array
type ElementType<T> = T extends (infer U)[] ? U : never;
type H = ElementType<string[]>;  // string
type I = ElementType<number[]>;  // number
type J = ElementType<string>;    // never

Inferring return types

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

Inferring parameter types

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]
// First parameter only
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type First = FirstParam<typeof createUser>;  // string

Unwrapping 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. Template literal types

Basics

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

String built-ins

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 shapes

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. Mapped types

Basic mapping

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

Key remapping

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

Filtering keys by value type

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. Practical examples

Example 1: Type-safe event bus

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()
});

Example 2: Typed actions for state

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]" }
});

Example 3: Type-safe query object

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. Advanced utility patterns

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

Summary

Takeaways

  1. Conditional types: T extends U ? X : Y
  2. infer: infer types inside branches (functions, arrays, Promise)
  3. Template literals: compose string literal types
  4. Mapped types: transform object types
  5. Key remapping: as in mapped types

Where this shows up

  • Type-safe event APIs
  • Discriminated unions and reducers
  • Query builders and ORM-style filters
  • Custom utilities beyond the standard library

Next steps

  • [TypeScript project: REST API](/en/blog/typescript-series-08-project/
  • [TypeScript utility types](/en/blog/typescript-series-05-utility-types/
  • [TypeScript generics](/en/blog/typescript-series-04-generics/


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Advanced TypeScript: conditional types, infer, template literals, mapped types, key remapping—type-safe events, reducers… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [TypeScript Decorators](/en/blog/typescript-series-06-decorators/
  • [TypeScript REST API Project | Express· Layered Architecture](/en/blog/typescript-series-08-project/
  • [TypeScript Utility Types | Partial· Pick](/en/blog/typescript-series-05-utility-types/

이 글에서 다루는 키워드 (관련 검색어)

TypeScript, Conditional Types, Template Literal Types, infer, Mapped Types 등으로 검색하시면 이 글이 도움이 됩니다.