Template Literal Types
Template Literal Types
JavaScript có template literal để ghép chuỗi ở runtime: `hello ${name}`. TypeScript 4.1 mang khả năng đó lên type level — bạn có thể ghép chuỗi trong type definition để tạo ra string literal types mới.
Tại sao cần? Vì rất nhiều API trong thực tế tuân theo quy tắc đặt tên dựa trên chuỗi: event handler là on${Event}, CSS class là prefix_${name}, route param là :paramName. Template Literal Types giúp bạn tự động sinh type cho tất cả biến thể mà không cần liệt kê tay.
#Vấn đề: Liệt kê string literal một cách thủ công
// ❌ Muốn type-safe cho event handler, phải liệt kê tay
type EventName = "click" | "focus" | "blur"
type EventHandlerName = "onClick" | "onFocus" | "onBlur"
// Thêm event mới? Phải cập nhật cả 2. Quên 1 cái là lỗi.Nếu bạn có 20 event, bạn phải viết 20 handler name. Thêm event thứ 21 mà quên handler — runtime crash, TypeScript không cứu được.
#Cú pháp cơ bản: `hello ${T}`
// ✅ Ghép string literal type
type Greeting = `hello ${"world"}`
// "hello world"
// ✅ Ghép với generic
type SayHello<T extends string> = `hello ${T}`
type Result = SayHello<"TypeScript">
// "hello TypeScript"Template literal type hoạt động giống template literal ở runtime, nhưng kết quả là một type, không phải giá trị.
#Built-in Utility Types: Uppercase, Lowercase, Capitalize, Uncapitalize
TypeScript cung cấp 4 built-in type cho phép biến đổi chuỗi ở type level:
type A = Uppercase<"hello"> // "HELLO"
type B = Lowercase<"HELLO"> // "hello"
type C = Capitalize<"hello"> // "Hello"
type D = Uncapitalize<"Hello"> // "hello"Kết hợp với template literal:
// ✅ Tạo event handler name từ event name
type OnEvent<E extends string> = `on${Capitalize<E>}`
type ClickHandler = OnEvent<"click"> // "onClick"
type FocusHandler = OnEvent<"focus"> // "onFocus"
type BlurHandler = OnEvent<"blur"> // "onBlur"Capitalize<"click"> cho "Click", rồi ghép với "on" thành "onClick". Không cần liệt kê tay.
#Sức mạnh thực sự: Kết hợp với unions
Khi bạn dùng union type trong template literal, TypeScript tự sinh tất cả permutation:
// ✅ Union × template literal = mọi tổ hợp
type Color = "red" | "blue" | "green"
type Size = "sm" | "md" | "lg"
type ColorSize = `${Color}-${Size}`
// "red-sm" | "red-md" | "red-lg" |
// "blue-sm" | "blue-md" | "blue-lg" |
// "green-sm" | "green-md" | "green-lg"
// → 9 kết quả, sinh tự độngQuay lại ví dụ event handler:
// ✅ Sinh tự động mọi event handler name
type EventName = "click" | "focus" | "blur" | "submit" | "change"
type EventHandler = `on${Capitalize<EventName>}`
// "onClick" | "onFocus" | "onBlur" | "onSubmit" | "onChange"
// ✅ Type-safe event handler map
type EventHandlerMap = {
[E in EventName as `on${Capitalize<E>}`]: (event: Event) => void
}
// {
// onClick: (event: Event) => void
// onFocus: (event: Event) => void
// onBlur: (event: Event) => void
// onSubmit: (event: Event) => void
// onChange: (event: Event) => void
// }Đây là sự kết hợp giữa mapped types và template literal types — mapped type duyệt qua key, as clause remap key bằng template literal.
#Ứng dụng thực tế: Type-safe event emitter
type EventMap = {
login: { userId: string }
logout: {}
error: { message: string; code: number }
}
// ✅ Tự động sinh method on(event, handler) type-safe
type EventEmitter<T extends Record<string, any>> = {
on<K extends keyof T>(
event: K,
handler: (payload: T[K]) => void
): void
emit<K extends keyof T>(event: K, payload: T[K]): void
}
declare const emitter: EventEmitter<EventMap>
// ✅ Type-safe — TypeScript biết payload đúng kiểu
emitter.on("login", (payload) => {
console.log(payload.userId) // ✅ biết userId là string
})
// ❌ Lỗi: "unknown" không phải key của EventMap
emitter.on("unknown", () => {})
// ❌ Lỗi: payload thiếu message
emitter.emit("error", { code: 500 })#Ứng dụng: Route path parser
// ✅ Trích xuất param name từ route path
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never
type Params = ExtractParams<"/users/:id/posts/:postId">
// "id" | "postId"infer cho phép bạn bắt một phần của chuỗi và dùng nó làm type. Đây là type-level pattern matching.
Kết hợp với mapped type để tạo param object:
// ✅ Tạo object type từ param names
type RouteParams<T extends string> = {
[K in ExtractParams<T>]: string
}
type UserPostParams = RouteParams<"/users/:id/posts/:postId">
// { id: string; postId: string }#Kết hợp nhiều template literal
Bạn có thể lồng và ghép nối nhiều template literal:
// ✅ HTTP method + path tạo thành route key
type Method = "GET" | "POST" | "PUT" | "DELETE"
type Path = "/users" | "/posts" | "/comments"
type RouteKey = `${Method} ${Path}`
// "GET /users" | "GET /posts" | ... | "DELETE /comments"
// → 12 kết quả
// ✅ CSS property với unit
type CSSUnit = "px" | "rem" | "em" | "%"
type CSSValue = `${number}${CSSUnit}`
// number ở type level không expand, nên đây là template literal pattern
// ✅ Nested object path
type NestedKey<T, Prefix extends string = ""> = {
[K in keyof T & string]: T[K] extends object
? NestedKey<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`
}[keyof T & string]
type UserPaths = NestedKey<{
name: string
address: {
city: string
zip: string
}
}>
// "name" | "address.city" | "address.zip"#Type inference với template literal: infer
Template literal types hỗ trợ infer để trích xuất phần tử từ chuỗi:
// ✅ Tách first name và last name
type ParseName<T extends string> =
T extends `${infer First} ${infer Last}`
? { first: First; last: Last }
: never
type Name = ParseName<"John Doe">
// { first: "John"; last: "Doe" }
// ✅ Parse file extension
type FileExtension<T extends string> =
T extends `${string}.${infer Ext}`
? Ext
: never
type Ext = FileExtension<"photo.jpg">
// "jpg"
type NoExt = FileExtension<"Makefile">
// never — không có dấu chấm#Anti-pattern: Template literal types quá phức tạp
// ❌ Đệ quy quá sâu — TypeScript sẽ báo lỗi
type TooDeep<T extends string> =
T extends `${infer A}${infer B}${infer C}${infer Rest}`
? `${A}_${B}_${C}_${TooDeep<Rest>}`
: T
// TypeScript giới hạn độ sâu đệ quy của template literal parsing.
// Với chuỗi dài, bạn sẽ thấy: "Type instantiation is excessively deep and possibly infinite."
// ✅ Giới hạn đệ quy bằng cách dừng sớm
type SafeSplit<T extends string, D extends string = "."> =
T extends `${infer Head}${D}${infer Tail}`
? Head | SafeSplit<Tail, D>
: T
type Result = SafeSplit<"a.b.c.d.e">
// "a" | "b" | "c" | "d" | "e" — depth = number of segments, manageableQuy tắc: giữ template literal parsing đơn giản, tối đa 2-3 level infer. Nếu logic quá phức tạp, cân nhắc dùng runtime parsing với type assertion.
#Anti-pattern: Dùng template literal type thay vì enum hoặc union
// ❌ Không cần thiết — dùng union đơn giản hơn
type StatusCode = `${"OK" | "NOT_FOUND" | "ERROR"}`
// Thực chất chỉ là "OK" | "NOT_FOUND" | "ERROR"
// Template literal ở đây không thêm giá trị
// ✅ Union trực tiếp luôn rõ ràng hơn
type StatusCode = "OK" | "NOT_FOUND" | "ERROR"
// Chỉ dùng template literal khi bạn cần BIẾN ĐỔI hoặc GHÉP NỐI chuỗi
type StatusWithPrefix = `status_${StatusCode}`
// "status_OK" | "status_NOT_FOUND" | "status_ERROR"#Tổng kết
- Template literal types ghép chuỗi ở type level:
`hello ${T}`. - 4 built-in utility:
Uppercase,Lowercase,Capitalize,Uncapitalize— biến đổi chữ hoa/thường. - Kết hợp với union types, template literal tự sinh tất cả permutation (Cartesian product).
infertrong template literal cho phép trích xuất phần tử từ chuỗi — useful cho parser types.- Kết hợp mapped types +
as+ template literal = sinh type cho event handler, route key, CSS class name một cách tự động. - Tránh đệ quy quá sâu — giới hạn parsing ở 2-3 level
infer. - Chỉ dùng template literal khi bạn cần biến đổi hoặc ghép nối chuỗi; nếu chỉ cần union string, viết union trực tiếp.
Bài tiếp theo: Conditional Types — T extends U ? X : Y, type-level if/else, và cách dùng infer để extract type từ bất kỳ cấu trúc nào.