ข้ามไปยังเนื้อหา

Template Literal Types

template literal type หน้าตาเหมือน template string ของ JavaScript แต่สร้าง type คุณ interpolate type อื่นเข้าไปใน type ที่มีรูปเป็น string

type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, Ada"; // ✅
const b: Greeting = "Hi, Ada"; // ❌ ต้องขึ้นต้นด้วย "Hello, "

interpolate union ของ literal เข้าไป แล้ว template จะ distribute บน union นั้น — สร้างทุกการผสมออกมาเป็น union ของ literal type:

type Lang = "en" | "th";
type Kind = "page" | "post";
type Key = `${Lang}-${Kind}`;
// ^? "en-page" | "en-post" | "th-page" | "th-post"

union สองตัวที่มี 2 member ให้ literal type 4 ตัว นี่คือวิธีที่ library generate string key แบบเป๊ะ — CSS property, event name, i18n key — โดยไม่มี runtime code เลย

TypeScript มาพร้อม type built-in 4 ตัวที่ transform string literal type: Uppercase, Lowercase, Capitalize และ Uncapitalize

type Loud = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"name">; // "Name"
// รวมกับ template เพื่อสร้างชื่อ event handler
type Handler<E extends string> = `on${Capitalize<E>}`;
type ClickHandler = Handler<"click">; // "onClick"
flowchart LR
  u["union: en or th"] -->|"interpolate into 'X-page'"| out["en-page or th-page"]
union distribute บน template literal type

พลังจริง ๆ มาเมื่อคุณรวม template literal กับ conditional type และ infer: คุณ destructure string type ได้ โดย capture ส่วนหนึ่งของตัวเองเข้าไปใน type variable ใหม่

// capture ชื่อ event หลัง prefix "on"
type EventName<T> = T extends `on${infer E}` ? E : never;
type E1 = EventName<"onClick">; // "Click"
type E2 = EventName<"onHover">; // "Hover"
type E3 = EventName<"click">; // never — ไม่มี prefix "on"
// แยก route เป็นส่วน ๆ
type Method<T> = T extends `${infer M} ${string}` ? M : never;
type M = Method<"GET /users">; // "GET"

infer E บอกว่า “match อะไรก็ได้ตรงนี้แล้วเรียกว่า E” นี่คือการ parse จริง ๆ ที่ระดับ type — เป็นเทคนิคเบื้องหลัง typed router ที่ดึง :id param จาก path string และ library ที่ validate format string

  • typed key: `${Theme}-${Shade}` สำหรับ type ของ design-token
  • event map: เปลี่ยน "click" เป็น "onClick" สำหรับ prop type
  • route param: ดึง id จาก "/users/:id" เป็น { id: string }
  • ORM/query builder: parse column string เป็น typed shape

ใช้เทคนิคนี้ตรงที่ string encode structure จริง ๆ ถ้าใช้กระตือรือร้นเกินไปคุณจะได้ type ที่เพื่อนร่วมทีมอ่านไม่ออก — เป็น trade-off ที่กลับมาเสมอของ type-level programming

type `${Lang}-${Kind}` ที่ `Lang = "en" | "th"` และ `Kind = "a" | "b"` คืออะไร?
`Capitalize<"click">` ให้อะไร?
ใน `T extends \`on${infer E}\` ? E : never` `infer E` ทำอะไร?
`EventName<"click">` คืนอะไรถ้า `EventName<T> = T extends \`on${infer E}\` ? E : never`?