Mapped Types
Mapped Types
Bạn có một interface User với 10 field. Bây giờ bạn cần một version mà tất cả field đều optional để dùng cho update. Bạn copy-paste rồi thêm ? vào từng field? Bạn cần thêm một version nữa mà tất cả field đều readonly? Lại copy-paste?
Đây chính là vấn đề Mapped Types giải quyết: biến đổi hàng loạt kiểu của mọi property trong một object type, chỉ với một công thức duy nhất.
#Vấn đề: Lặp lại kiểu một cách thủ công
// ❌ Thủ công — muốn khóc khi User có 20 field
interface User {
name: string
age: number
email: string
}
interface PartialUser {
name?: string
age?: number
email?: string
}
interface ReadonlyUser {
readonly name: string
readonly age: number
readonly email: string
}Mỗi lần User thay đổi, bạn phải cập nhật cả 3 interface. Không maintainable.
#Cú pháp cơ bản: { [K in keyof T]: NewType }
Mapped Types duyệt qua từng key của T và áp dụng một phép biến đổi:
// ✅ Mọi property của T đều trở thành string
type AllString<T> = { [K in keyof T]: string }
// Dùng thử
type UserAsString = AllString<User>
// { name: string; age: string; email: string }Cách đọc: "Với mỗi key K trong tập hợp keyof T, tạo property K với kiểu NewType."
Phân tích từng phần:
K in keyof T— duyệt qua union của tất cả key. NếuT = UserthìKlần lượt là"name" | "age" | "email".: NewType— gán kiểu mới cho mỗi property đó.- Kết quả là một object type mới có đúng các key cũ nhưng kiểu value khác.
#Modifier: readonly và ?
TypeScript cho phép bạn thêm hoặc bớt modifier readonly và ? trong mapped types:
// ✅ Thêm readonly vào mọi property
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }
// ✅ Thêm ? (optional) vào mọi property
type MyPartial<T> = { [K in keyof T]?: T[K] }
// ✅ Bỏ ? (bắt buộc lại mọi field)
type MyRequired<T> = { [K in keyof T]-?: T[K] }
// ✅ Bỏ readonly
type Mutable<T> = { -readonly [K in keyof T]: T[K] }Dấu - phía trước modifier nghĩa là loại bỏ modifier đó. Không có dấu - nghĩa là thêm modifier.
// ✅ Sử dụng
interface Config {
readonly host: string
readonly port: number
}
type MutableConfig = Mutable<Config>
// { host: string; port: number } — readonly đã bị loại bỏ
type StrictConfig = Required<{ host?: string; port?: number }>
// { host: string; port: string } — ? đã bị loại bỏ#Tự viết lại Partial, Required, Readonly
Đây là cách chính xác mà TypeScript implement các utility type này trong lib.es5.d.ts:
// ✅ Chính xác như Partial<T> built-in
type MyPartial<T> = {
[K in keyof T]?: T[K]
}
// ✅ Chính xác như Required<T> built-in
type MyRequired<T> = {
[K in keyof T]-?: T[K]
}
// ✅ Chính xác như Readonly<T> built-in
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}Chúng hoạt động vì T[K] giữ nguyên kiểu gốc — bạn chỉ thay đổi modifier, không thay đổi kiểu value.
#Tự viết Pick<T, K>
Pick thú vị hơn: nó không duyệt qua tất cả key, mà chỉ lấy một tập con key:
// ✅ Chỉ lấy các key K từ T
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
// Sử dụng
type UserBasic = MyPick<User, "name" | "email">
// { name: string; email: string }K extends keyof T là constraint đảm bảo bạn chỉ được pick key có thật trong T. Thử pick key không tồn tại sẽ bị lỗi compile.
// ❌ Lỗi: "phone" không phải key của User
type Invalid = MyPick<User, "name" | "phone">#Key Remapping với as clause
TypeScript 4.1引入 as clause cho phép bạn đổi tên key trong mapped type:
// ✅ Đổi tên key bằng as
type RenameKeys<T> = {
[K in keyof T as `new_${string & K}`]: T[K]
}
type RenamedUser = RenameKeys<User>
// { new_name: string; new_age: number; new_email: string }string & K cần thiết vì K có thể là string | number | symbol, nhưng template literal chỉ chấp nhận string.
Một pattern hữu ích: lọc key bằng as bằng cách map sang never:
// ✅ Chỉ giữ key có value là string
type OnlyStringValues<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
type UserStrings = OnlyStringValues<User>
// { name: string; email: string } — age (number) bị loạiKhi as trả về never, key đó bị loại bỏ khỏi kết quả.
#Kết hợp mapped types với utility types khác
Mapped types phát huy sức mạnh khi kết hợp:
// ✅ DeepReadonly — readonly đệ quy
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K]
}
interface Nested {
config: {
host: string
settings: {
debug: boolean
}
}
}
type FrozenNested = DeepReadonly<Nested>
// config, config.settings, config.settings.debug đều readonly// ✅ Nullable — biến mọi value thành T | null
type Nullable<T> = { [K in keyof T]: T[K] | null }
type NullableUser = Nullable<User>
// { name: string | null; age: number | null; email: string | null }#Anti-pattern: Overusing mapped types khi type alias đơn giản là đủ
// ❌ Over-engineered — dùng mapped type không cần thiết
type Status = { [K in "loading" | "error" | "success"]: boolean }
// ✅ Đơn giản hơn, dễ đọc hơn
type Status = Record<"loading" | "error" | "success", boolean>
// Hoặc thậm chí:
type Status = {
loading: boolean
error: boolean
success: boolean
}Quy tắc: nếu bạn không biến đổi từ một type có sẵn, bạn không cần mapped type. Chỉ dùng mapped types khi:
- Bạn có một type
Tsẵn và muốn tạo version biến đổi từ nó. - Bạn muốn logic thay đổi tự động khi
Tthay đổi.
Nếu bạn chỉ muốn định nghĩa một object type cố định, viết ra trực tiếp luôn rõ ràng hơn.
#Anti-pattern: Quên constraint khi dùng key remapping
// ❌ Có thể lỗi runtime nếu K không phải string
type Bad<T> = {
[K in keyof T as `prefix_${K}`]: T[K]
}
// ✅ Thêm constraint để an toàn
type Safe<T> = {
[K in keyof T as `prefix_${string & K}`]: T[K]
}string & K là pattern bắt buộc khi dùng template literal với key remapping. Thiếu nó, TypeScript sẽ báo lỗi vì keyof T có thể bao gồm number | symbol.
#Tổng kết
- Mapped Types duyệt qua mỗi key của
Tvà áp dụng phép biến đổi:{ [K in keyof T]: ... }. - Dùng
-readonlyvà-?để loại bỏ modifier; dùngreadonlyvà?để thêm modifier. T[K]giữ nguyên kiểu gốc — mapped type chỉ thay đổi modifier hoặc ánh xạ sang kiểu mới.- Key remapping với
ascho phép đổi tên key hoặc lọc key bằng cách map sangnever. - Constraint
K extends keyof Tgiúp giới hạn key hợp lệ;string & Kcần thiết khi dùng template literal. - Đừng dùng mapped type khi type alias đơn giản là đủ — chỉ dùng khi bạn biến đổi từ type có sẵn.
Bài tiếp theo: Template Literal Types — ghép chuỗi ở type level, kết hợp với mapped types để sinh ra các type phức tạp như event handler names và route paths.