Advanced TypeScript | Conditional Types, Template Literals, infer

Advanced TypeScript | Conditional Types, Template Literals, infer

이 글의 핵심

Level up the type system: conditional types, infer in conditional branches, string template types, mapped types with key remapping, and DeepPartial-style utilities for safer libraries.

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