Category: guide
TypeScript Conditional Types — infer, Mapped Types, Template Literal Types
เทคนิค TypeScript ระดับสูง: conditional types, infer keyword, mapped types modifiers, template literal types
สารบัญ
Conditional Types พื้นฐาน
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
type C = IsString<string>; // true
Syntax: T extends U ? X : Y — ถ้า T assignable ให้ U ได้ → X ไม่งั้น → Y
Distributive Conditional Types
เมื่อ T เป็น union type conditional type จะกระจาย (distribute) ไปทีละ member:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] (กระจาย ไม่ใช่ (string | number)[])
ปิด distributive behavior ด้วย tuple:
type NoDistribute<T> = [T] extends [any] ? T[] : never;
type Result = NoDistribute<string | number>;
// (string | number)[] (ไม่กระจาย)
infer — Extract Type จาก Structure
// ดึง return type ของ function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { id: 1, name: 'Alice' }; }
type User = ReturnType<typeof getUser>;
// { id: number; name: string; }
// ดึง element type ของ array
type ElementType<T> = T extends (infer E)[] ? E : never;
type E = ElementType<string[]>; // string
type F = ElementType<[1, 2, 3]>; // 1 | 2 | 3
infer กับ Promise
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
(TypeScript 4.5+ มี built-in Awaited<T>)
infer กับ Function Parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
function greet(name: string, age: number): void {}
type Params = Parameters<typeof greet>; // [string, number]
type First = FirstParam<typeof greet>; // string
Mapped Types
Transform แต่ละ property ของ type:
// ทำ properties ทั้งหมดเป็น optional
type Partial<T> = { [K in keyof T]?: T[K]; };
// ทำ properties ทั้งหมดเป็น readonly
type Readonly<T> = { readonly [K in keyof T]: T[K]; };
// Map เป็น type ใหม่
type Nullable<T> = { [K in keyof T]: T[K] | null; };
Mapped Type Modifiers
type User = { id: number; name?: string; readonly email: string };
// ลบ optional (?) และ readonly ออก
type Required<T> = { [K in keyof T]-?: T[K]; }; // -? ลบ optional
type Mutable<T> = { -readonly [K in keyof T]: T[K]; }; // -readonly
type A = Required<User>;
// { id: number; name: string; email: string } — name ไม่ optional แล้ว
type B = Mutable<User>;
// { id: number; name?: string; email: string } — email ไม่ readonly แล้ว
Key Remapping ด้วย as
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type User = { name: string; age: number };
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
Template Literal Types
type EventName = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// ใช้กับ object
type EventHandlers = {
[K in EventName as `on${Capitalize<K>}`]: (event: Event) => void;
};
// { onClick: ...; onFocus: ...; onBlur: ...; }
Template Literal + infer
// Parse route params จาก string
type ExtractParams<Route extends string> =
Route extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Route extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'
Recursive Conditional Types
// Deep partial — optional recursively
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type Config = { db: { host: string; port: number }; debug: boolean };
type PartialConfig = DeepPartial<Config>;
// { db?: { host?: string; port?: number }; debug?: boolean }
Utility Types ที่ใช้ Conditional Types
TypeScript มี built-in หลายตัวที่ใช้ conditional types:
// Extract และ Exclude
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
type A = Extract<'a' | 'b' | 'c', 'a' | 'c'>; // 'a' | 'c'
type B = Exclude<'a' | 'b' | 'c', 'a' | 'c'>; // 'b'
// NonNullable
type NonNullable<T> = T extends null | undefined ? never : T;
type C = NonNullable<string | null | undefined>; // string
Pattern: Type-safe Event Emitter
type Events = {
'user:login': { userId: string; timestamp: number };
'user:logout': { userId: string };
'error': Error;
};
class TypedEmitter<T extends Record<string, any>> {
on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {}
emit<K extends keyof T>(event: K, data: T[K]): void {}
}
const emitter = new TypedEmitter<Events>();
emitter.on('user:login', (data) => {
console.log(data.userId); // ✓ TypeScript รู้ว่ามี userId
console.log(data.foo); // ✗ error — foo ไม่มีใน user:login
});
สรุป Pattern ที่ใช้บ่อย
| Pattern | ใช้เมื่อ |
|---|---|
T extends U ? X : Y | เลือก type ตาม constraint |
infer R | ดึง type ออกจาก structure |
[K in keyof T] | transform ทุก property |
-? / -readonly | ลบ modifier |
as \prefix${K}“ | rename keys |
| Template literal | สร้าง string type ใหม่ |
| Recursive | deep transform |