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
- Conditional types:
T extends U ? X : Y - infer: infer types inside branches (functions, arrays,
Promise) - Template literals: compose string literal types
- Mapped types: transform object types
- Key remapping:
asin 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
Related posts
- Kotlin advanced features | DSL, reflection, annotations
- Python decorators | @decorator explained
- TypeScript getting started | install, config, syntax
- Advanced TypeScript types | unions, intersections, literals
- TypeScript interfaces | complete guide