ข้ามไปเนื้อหาหลัก

Category: guide

TypeScript Conditional Types — infer, Mapped Types, Template Literal Types

เทคนิค TypeScript ระดับสูง: conditional types, infer keyword, mapped types modifiers, template literal types

· อ่านประมาณ 5 นาที

สารบัญ

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 ใหม่
Recursivedeep transform