TS Type Challenges · Part 2 — Mapped Types
Part 2: master the mapped type. Rebuild Partial, Required, Mutable, Readonly 2, PartialByKeys, Merge and ReplaceKeys; add and remove the readonly/? modifiers; and key-remap with as — each with a toggle-to-reveal answer.
Đây là Phần 2 của series 12 bài về TypeScript type challenges. Ở Phần 1 ta gặp năm khối nền tảng; ở đây ta luyện sâu khối đầu tiên: mapped type.
Mapped type lặp qua một union các key và dựng object từ mỗi key. Chỉ một ý tưởng đó — cộng hai modifier (readonly, ?) và đổi key (as) — đã tạo nên hầu hết utility type built-in của TypeScript.
Ghi nguồn: các đáp án theo cách giải chuẩn của cộng đồng type-challenges (giấy phép MIT); số khớp ID chính thức.
Hình dạng của mọi mapped type
Ghi nhớ khuôn mẫu này — mọi thứ bên dưới chỉ là biến thể của nó:
type Mapped<T> = {
[K in keyof T]: T[K];
// ▲ modifier ▲ key source ▲ value
// (readonly/?) (a union of keys) (usually indexed access)
};
Trình biên dịch thực sự làm gì
Khi TypeScript thấy [K in keyof T], nó không chạy vòng lặp runtime. Nó mở rộng mapped type lúc biên dịch: lấy union các key, duyệt từng thành viên, rồi lắp một object type mới. Hãy coi như for...of trên danh sách key ở tầng type.
interface Point { x: number; y: number }
// step 1: keyof Point → 'x' | 'y'
// step 2: loop K = 'x' → slot x: Point['x'] = number
// step 3: loop K = 'y' → slot y: Point['y'] = number
// step 4: assemble → { x: number; y: number }
type Copy<Point> = { [K in keyof Point]: Point[K] };
Ba núm bạn có thể vặn:
- Modifier trước key: thêm
readonly/?, hoặc gỡ bằng dấu-đứng trước. - Nguồn key: bất kỳ union nào của
string | number | symbol— thường làkeyof T, nhưng có thể là tập con. - Value: type bất kỳ, thường là
T[K](indexed access), nhưng bạn có thể biến đổi.
Cạm bẫy: homomorphic mapped type
Khi nguồn key đúng là keyof T và value là T[K], TypeScript coi kết quả là homomorphic — nó giữ modifier readonly và ? từ bản gốc. Đổi nguồn key (vd. sang union tập con) hoặc biểu thức value, việc giữ modifier dừng lại — đó là lý do “làm X chỉ với một số key” thường cần công thức chia-rồi-giao.
Tóm tắt: { [K in Keys]: Value } là vòng lặp compile-time trên union key; modifier và as là cú pháp bổ sung.
2757 · PartialByKeys (và Partial thường)
Mục tiêu
Chỉ biến các key liệt kê trong K thành optional; giữ mọi key khác y hệt trên T. Khi bỏ qua K, hành xử như Partial built-in và làm mềm tất cả.
Vì sao không hiển nhiên: Modifier ? của mapped type áp cho mọi key trong vòng lặp — bạn không thể nói “chỉ thêm ? khi K khớp” trong một [K in keyof T]? duy nhất.
Khởi động trước — biến mọi thuộc tính thành optional:
type MyPartial<T> = { [K in keyof T]?: T[K] };
? thêm modifier optional cho mọi key.
interface User {
name: string;
age: number;
address: string;
}
type R = PartialByKeys<User, 'name'>;
// Expected: { name?: string; age: number; address: string }
Cách thử ngây thơ (và vì sao hỏng)
// ❌ Makes ALL keys optional — the ? sits outside the conditional
type PartialByKeys<T, K extends keyof T> = {
[P in keyof T]?: P extends K ? T[P] : T[P];
};
? gắn vào pattern key được map, không phải từng nhánh conditional ở vế value. TypeScript không diễn đạt được “chỉ optional cho một số key” trong một lượt homomorphic.
Hiện đáp án
type PartialByKeys<T, K extends keyof T = keyof T> = Omit<T, K> &
Partial<Pick<T, K>> extends infer O
? { [P in keyof O]: O[P] }
: never;Cơ chế: chia → biến đổi → giao → làm đẹp
Thay vì ép một mapped type, chia T thành hai object type, chỉ biến đổi nửa cần thiết, rồi giao lại:
- key không thuộc
K, giữ nguyên. - key thuộc
K, làm optional bằng mapped type thường. - gộp hai nửa thành một type.
- làm đẹp: map lại intersection thành một object phẳng để hover gọn hơn.
Từng dòng
- ràng buộc
Klà key thật củaT; mặc định mọi key để bỏKra đượcPartialđầy đủ. - utility built-in bỏ các key trong
K. Pickgiữ chỉK,Partialthêm?cho mọi key trong tập con đó.- giao: object phải thỏa cả hai nửa.
- sao chép key của
Ovào một object type (map đồng nhất làm phẳng hiển thị).
Đánh giá type từng bước
Input cụ thể: PartialByKeys<User, 'name'>:
interface User { name: string; age: number; address: string }
// step 1: K = 'name'
// step 2: Omit<User, 'name'>
// → { age: number; address: string }
// step 3: Pick<User, 'name'>
// → { name: string }
// step 4: Partial<Pick<User, 'name'>>
// → { [P in 'name']?: string } → { name?: string }
// step 5: intersect
// → { age: number; address: string } & { name?: string }
// step 6: prettify — loop P over keyof O
// P = 'age' → age: number
// P = 'address'→ address: string
// P = 'name' → name?: string
// step 7: final R
// → { age: number; address: string; name?: string }
type R = PartialByKeys<User, 'name'>;Cạm bẫy
- Giữ homomorphic:
{ [P in keyof T]?: T[P] }sao chép optional/readonly từT; chia bằngOmit/Picktránh điều đó khi cần modifier không đối xứng. - Hiển thị intersection:
{ a: 1 } & { b?: 2 }đúng nhưng xấu khi hover — prettify sửa hiển thị, không đổi ngữ nghĩa. - Mặc định
K:K extends keyof T = keyof Ttừ chối kiểuPartialByKeys<User, 'email'>lúc biên dịch.
Tóm tắt: không bật ? theo từng key trong một map — chia bằng Omit/Pick, làm mềm một nửa, giao, làm đẹp.
2759 · RequiredByKeys (và Required thường)
Mục tiêu
Đối xứng PartialByKeys: chỉ biến key trong K thành bắt buộc; giữ optional của key khác.
Vì sao không hiển nhiên: -? (gỡ optional) áp cho mọi key trong vòng map, giống ? áp cho tất cả. Cần cùng mẹo chia-rồi-giao, đổi Partial thành Required.
Bản thường trước — gỡ optional khỏi tất cả:
type MyRequired<T> = { [K in keyof T]-?: T[K] };
Dấu -? đứng đầu gỡ modifier optional khỏi mỗi key.
type R = RequiredByKeys<{ name?: string; age?: number }, 'name'>;
// Expected: { name: string; age?: number }
Cách thử ngây thơ (và vì sao hỏng)
// ❌ Removes ? from EVERY key — age becomes required too
type RequiredByKeys<T, K extends keyof T> = {
[P in keyof T]-?: P extends K ? T[P] : T[P];
};
Modifier -? không có điều kiện; nó kích hoạt cho mọi P trong vòng lặp.
Hiện đáp án
type RequiredByKeys<T, K extends keyof T = keyof T> = Omit<T, K> &
Required<Pick<T, K>> extends infer O
? { [P in keyof O]: O[P] }
: never;Cơ chế
Khung y hệt PartialByKeys:
- key ngoài
K, giữ optional gốc. { [P in keyof Pick<T,K>]-?: ... }gỡ?chỉ từ key trongK.&+ prettify — giống trước.
-? là núm gỡ optional; Required chỉ là mapped type built-in bật nó cho mọi key nó thấy.
Đánh giá type từng bước
type Input = { name?: string; age?: number }
// step 1: K = 'name'
// step 2: Omit<Input, 'name'>
// → { age?: number }
// step 3: Pick<Input, 'name'>
// → { name?: string }
// step 4: Required<Pick<Input, 'name'>>
// → { [P in 'name']-?: string } → { name: string }
// step 5: intersect
// → { age?: number } & { name: string }
// step 6: prettify
// → { name: string; age?: number }
type R = RequiredByKeys<Input, 'name'>;Cạm bẫy
-?chỉ gỡ, không thêm key: nếuKđặt tên key không có trênT,Picklà{}và không có gì mới.- Bảng modifier (bốn thao tác):
[K in keyof T]: T[K] // copy as-is
readonly [K ...]: T[K] // add readonly
-readonly [K ...]: T[K] // remove readonly
[K ...]?: T[K] // add optional
[K ...]-?: T[K] // remove optionalTóm tắt: “chia → biến đổi một phần → giao → làm đẹp” là công thức tái dùng cho mọi bài “làm X chỉ với các key này”.
2793 · Mutable
Mục tiêu
Gỡ readonly khỏi mọi thuộc tính — ngược với Readonly<T> built-in.
type R = Mutable<{ readonly a: 1; readonly b: 2 }>;
// Expected: { a: 1; b: 2 }
Vì sao không hiển nhiên: readonly là modifier thuộc tính, không phải type bạn “bóc” bằng T[K]. Bản sao thường { [K in keyof T]: T[K] } là homomorphic và giữ readonly.
Cách thử ngây thơ (và vì sao hỏng)
// ❌ Homomorphic — readonly survives the copy
type Mutable<T> = { [K in keyof T]: T[K] };
type Bad = Mutable<{ readonly a: 1 }>;
// Still: { readonly a: 1 }
Phải dùng modifier mapped type -readonly để gỡ tính bất biến một cách tường minh.
Hiện đáp án
type Mutable<T> = { -readonly [K in keyof T]: T[K] };Từng dòng
- lặp qua mọi key của
T. -readonlytrước pattern key — gỡ modifierreadonlykhỏi mỗi thuộc tính được map.T[K]— giữ nguyên type value (chỉ cờ mutability đổi).
Như -? gỡ optional, -readonly gỡ tính bất biến.
Đánh giá type từng bước
type Input = { readonly a: 1; readonly b: 2 }
// step 1: keyof Input → 'a' | 'b'
// step 2: K = 'a', apply -readonly → a: 1 (writable)
// step 3: K = 'b', apply -readonly → b: 2 (writable)
// step 4: assemble
// → { a: 1; b: 2 }
type R = Mutable<Input>;Cạm bẫy
- Bản sao homomorphic giữ
readonly: map đồng nhất không đủ; cần-readonlytường minh. - Chỉ nông:
Mutablegỡreadonlytầng trên; object lồng vẫn readonly trừ khi bạn đệ quy. - Biến thể “theo key”: dùng
Omit<T, K> & Mutable<Pick<T, K>>— cùng công thức chia nhưPartialByKeys.
Tóm tắt: -readonly là núm mapped type gỡ tính bất biến từng key.
8 · Readonly 2
Mục tiêu
Chỉ biến key trong K thành readonly; giữ key khác ghi được. Mặc định K là mọi key để khi bỏ qua hành xử như Readonly<T>.
interface Todo {
title: string;
description: string;
completed: boolean;
}
type R = MyReadonly2<Todo, 'title' | 'description'>;
// title & description become readonly; completed stays writable
Vì sao không hiển nhiên: readonly trên key được map áp cho mọi key trong vòng lặp — không đóng băng chỉ tập con trong một lượt.
Cách thử ngây thơ (và vì sao hỏng)
// ❌ Freezes EVERY key, not just K
type MyReadonly2<T, K extends keyof T> = {
readonly [P in keyof T]: T[P];
};
Hiện đáp án
type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> &
Readonly<Pick<T, K>>;Cơ chế: chia-rồi-giao (không cần prettify ở đây)
- key ghi được giữ nguyên.
{ readonly [P in keyof Pick<T,K>]: ... }thêmreadonlychỉ cho key đã chọn.- một object thỏa cả hai nửa.
Từng dòng
- ràng buộc
Klà key hợp lệ; mặc định “mọi key”. - bỏ key đóng băng khỏi nửa này.
- mapped type built-in với modifier
readonlytrên mọi key nó thấy. - giao nửa ghi được và readonly.
Đánh giá type từng bước
interface Todo {
title: string;
description: string;
completed: boolean;
}
// step 1: K = 'title' | 'description'
// step 2: Omit<Todo, 'title' | 'description'>
// → { completed: boolean }
// step 3: Pick<Todo, 'title' | 'description'>
// → { title: string; description: string }
// step 4: Readonly<Pick<...>>
// → { readonly title: string; readonly description: string }
// step 5: intersect
// → { completed: boolean }
// & { readonly title: string; readonly description: string }
// step 6: final (intersection is already readable here)
// → { completed: boolean; readonly title: string; readonly description: string }
type R = MyReadonly2<Todo, 'title' | 'description'>;Cạm bẫy
K extends keyof T = keyof TvsK = keyof T: mệnh đềextendstừ chối key sai như'foo'khifookhông có trênT.- Không bắt buộc prettify: khác
PartialByKeys, intersection này thường hover gọn; thêm prettify chỉ khiEqualtrong test phàn nàn. - Đóng băng nông: object lồng trong key đóng băng vẫn mutable ở tầng của chúng.
Tóm tắt: chỉ đóng băng K bằng Omit + Readonly<Pick<...>> + &.
599 · Merge
Mục tiêu
Gộp hai object type thành một. Khi key có ở cả hai, type thứ hai (S) thắng.
type Foo = { a: number; b: string };
type Bar = { b: number; c: boolean };
type R = Merge<Foo, Bar>;
// Expected: { a: number; b: number; c: boolean }
Vì sao không hiển nhiên: F & S giao value trên key chung (b thành string & number → thường never), không phải “bên sau thắng”. Cần mapped type tự viết duyệt union tập key và chọn value bằng conditional.
Cách thử ngây thơ (và vì sao hỏng)
// ❌ Intersection merges values, not keys — shared keys become intersections
type Merge<F, S> = F & S;
type Bad = Merge<{ b: string }, { b: number }>;
// b: string & number → never
// ❌ Also wrong — keyof (F & S) still pairs with intersected values
type Merge<F, S> = { [K in keyof (F & S)]: (F & S)[K] };
// b: never again
Hiện đáp án
type Merge<F, S> = {
[K in keyof F | keyof S]: K extends keyof S
? S[K]
: K extends keyof F
? F[K]
: never;
};Cơ chế
- Nguồn key
keyof F | keyof S— union hai tập key (không dùngkeyof (F & S)để chọn value). - Conditional theo key — với mỗi
K, kiểm traStrước (ghi đè), rồiF, rồinever. - Thu hẹp trước khi index —
K extends keyof S ? S[K]đảm bảoKlà key hợp lệ củaStrước khi index.
Từng dòng
- lặp qua mọi key xuất hiện ở một trong hai object.
- nếu
Scó key, lấy type củaS(bên sau thắng). - ngược lại nếu chỉ
Fcó, lấyF[K]. : never— nhánh dự phòng không tới cho type checker.
Đánh giá type từng bước
type Foo = { a: number; b: string }
type Bar = { b: number; c: boolean }
// step 1: keyof Foo | keyof Bar → 'a' | 'b' | 'c'
// step 2: K = 'a'
// 'a' extends keyof Bar? no
// 'a' extends keyof Foo? yes → Foo['a'] → number
// step 3: K = 'b'
// 'b' extends keyof Bar? yes → Bar['b'] → number (S wins!)
// step 4: K = 'c'
// 'c' extends keyof Bar? yes → Bar['c'] → boolean
// step 5: assemble
// → { a: number; b: number; c: boolean }
type R = Merge<Foo, Bar>;Cạm bẫy
- Không viết
F[K]bừa: khiKlà'c',Kcó thể không extendkeyof F— conditional thu hẹp trước. - Linh hoạt nguồn key: mapped type nhận bất kỳ union
string | number | symbol— lần đầu ta rờikeyof T. keyof (F & S)vskeyof F | keyof S: với object thường tập key trùng, nhưngF & Sđã giao value xung đột — dựng key tường minh giữ logic ghi đè rõ.
Tóm tắt: duyệt keyof F | keyof S và để conditional chọn S[K] trước F[K].
1130 · ReplaceKeys (đổi key bằng as)
Mục tiêu
Cho union các object, thay type của key được nêu bằng type từ object tra cứu Y — nhưng chỉ trên thành viên thực sự có key đó.
type A = { type: 'a'; name: string; flag: number };
type B = { type: 'b'; id: number; flag: number };
type R = ReplaceKeys<A | B, 'name' | 'flag', { name: number; flag: string }>;
// A becomes { type:'a'; name:number; flag:string }
// B becomes { type:'b'; id:number; flag:string } (no 'name' to replace)
Vì sao không hiển nhiên: U là union (A | B), và mapped type phân phối trên union object — mỗi thành viên biến đổi độc lập. Phải rẽ nhánh theo key ở vế value; as dùng đổi tên/bỏ key, không phải đổi type.
Cách thử ngây thơ (và vì sao hỏng)
// ❌ Y[K] when K might not be a key of Y — error or wrong inference
type ReplaceKeys<U, T, Y> = {
[K in keyof U]: K extends T ? Y[K] : U[K];
};
// Error: Type 'K' cannot be used to index type 'Y'
TypeScript không cho index Y bằng K cho đến khi bạn chứng minh K extends keyof Y.
Hiện đáp án
type ReplaceKeys<U, T, Y> = {
[K in keyof U]: K extends T ? (K extends keyof Y ? Y[K] : never) : U[K];
};Cơ chế
[K in keyof U]— với mỗi thành viên union, lặp key của thành viên đó (distribution xử lýAvàBriêng).K extends T ? ... : U[K]— đây có phải key cần thay? Nếu không, giữ gốc.K extends keyof Y ? Y[K] : never— nếu có, tra type mới trongY; nếuYthiếu key, ranever.
Từng dòng
U— input union; mapped type phân phối:ReplaceKeys<A|B, ...>→ReplaceKeys<A,...> | ReplaceKeys<B,...>.T— union tên key cần thay (vd.'name' | 'flag').Y— object tra cứu chứa type thay thế.- Conditional lồng ở vế value — đổi type không cần
as.
Đánh giá type từng bước
Thành viên A trước (phân phối):
type A = { type: 'a'; name: string; flag: number }
type Y = { name: number; flag: string }
// step 1: ReplaceKeys<A, 'name'|'flag', Y> — single member, no union left
// step 2: K = 'type' → 'type' extends 'name'|'flag'? no → A['type'] → 'a'
// step 3: K = 'name' → yes, in Y → Y['name'] → number
// step 4: K = 'flag' → yes, in Y → Y['flag'] → string
// step 5: → { type: 'a'; name: number; flag: string }Thành viên B:
type B = { type: 'b'; id: number; flag: number }
// step 1: K = 'type' → not in T → 'b'
// step 2: K = 'id' → not in T → number
// step 3: K = 'flag' → in T, in Y → string
// step 4: → { type: 'b'; id: number; flag: string }
// (no 'name' key on B — never touched)Kết quả: ReplaceKeys<A, ...> | ReplaceKeys<B, ...>.
Cạm bẫy
- Phân phối trên
U: input union là lý doBkhông bao giờ có keyname— mỗi hình object chỉ map trên key của chính nó. asvs conditional value: dùngasđể đổi tên hoặc lọc key (map sangneverđể bỏ); dùng conditional vế value để đổi type.- Thu hẹp đôi:
K extends TrồiK extends keyof Y— cần cả hai trước khiY[K]hợp lệ.
Tóm tắt: phân phối trên U, rẽ nhánh theo key ở vế value, thu hẹp trước khi index Y.
Bài tập
1. Viết Clone<T> sao chép y hệt object type nhưng hiển thị phẳng khi rê chuột (dùng mẹo làm đẹp).
Lời giải
type Clone<T> = { [K in keyof T]: T[K] };Map mọi key về chính value của nó vốn là phép đồng nhất; bản thân việc map đã làm phẳng intersection.
Với type đã là intersection, bọc bằng đuôi prettify: T extends infer O ? { [P in keyof O]: O[P] } : never.
2. Vì sao Merge dùng keyof F | keyof S làm nguồn key thay vì keyof (F & S)?
Lời giải
keyof (F & S) thực ra cùng union, nhưng suy luận trên F & S cho key đụng nhau thì mập mờ (value đã là intersection). Tự dựng tập key và tự chọn value giữ quy tắc ghi đè (“bên sau thắng”) rõ ràng và trong tầm kiểm soát.
Nâng cao:cài MutableByKeys<T, K> — bằng cùng công thức chia-rồi-giao với Omit + Mutable<Pick<T, K>>.
Điểm chính
- Mapped type là
{ [K in Keys]: Value }— trình biên dịch mở rộng từng key lúc compile; vặn ba núm: modifier, nguồn key, value. readonly/?thêm modifier;-readonly/-?gỡ chúng.- “Làm X chỉ với một số key” = chia bằng
Omit/Pick, biến đổi một phần, giao, rồi làm đẹp. - Nguồn key có thể là bất kỳ union key nào, không chỉ
keyof T. - Map homomorphic giữ modifier; thay đổi không đối xứng theo key cần chia-rồi-giao hoặc conditional vế value.
Tiếp theo
Phần 3 — Conditional Type & Distribution: cỗ máy A extends B ? X : Y. Ta dựng If, IsNever, IsUnion, AnyOf, chốt hạ quy tắc distributive (và cách tắt bằng [T]), và giải mã “đồ nghề” Equal nổi tiếng đứng sau mọi test.