본문으로 건너뛰기 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: "hong@test.com" }
});

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



Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Advanced TypeScript: conditional types, infer, template literals, mapped types, key remapping—type-safe events, reducers…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers TypeScript, Conditional Types, Template Literal Types, infer, Mapped Types.