Bài 8 · Nâng cao · 26 phút
Mapped & template literal types
Biên soạn bởi Nguyễn Anh Tuấn
Sinh kiểu từ kiểu: mapped type ({ [K in keyof T]: ... }), đổi tên khoá (key remapping) và thêm/bớt modifier readonly/optional; template literal type để ghép chuỗi ở tầng kiểu (vd dựng tên onClick từ click). Cách các kiểu thông minh của thư viện được dựng nên.
Mapped type cho phép DUYỆT qua mọi khoá của một kiểu rồi sinh ra kiểu mới.
Cú pháp [K in keyof T] đọc là "với mỗi khoá K của T". Dạng đơn giản
nhất chỉ sao chép lại:
mapped-co-ban.ts
interface Meo { ten: string; tuoi: number; }
type Sao<T> = { [K in keyof T]: T[K] };
// Sao<Meo> = { ten: string; tuoi: number } (ban sao y het)
type ChiDoc<T> = { readonly [K in keyof T]: T[K] };
// ChiDoc<Meo> = { readonly ten: string; readonly tuoi: number } Bấm thử các mapped type áp lên cùng một kiểu Meo:
Kiểu nguồn: type Meo = { ten: string; tuoi: number }. Chọn một mapped type áp lên nó:
{ [K in keyof T]: T[K] }
Kết quả khi áp lên Meo:
{
ten: string;
tuoi: number;
} Duyệt mọi khoá và giữ nguyên - một bản sao y hệt.
- ▸keyof T cho union các khoá của T; [K in keyof T] duyệt từng khoá đó.
- ▸T[K] là kiểu của field K (indexed access), dùng để giữ hoặc đổi kiểu giá trị.
- ▸Mapped type sinh một kiểu MỚI từ kiểu cũ, không sửa kiểu cũ.
Trong lúc map, bạn gắn thêm readonly hay ? cho mọi field - hoặc GỠ chúng bằng dấu -:
modifier.ts
type CoCung<T> = { [K in keyof T]?: T[K] };
// CoCung<Meo> = { ten?: string; tuoi?: number } (moi field optional)
type GoReadonly<T> = { -readonly [K in keyof T]: T[K] }; // BO readonly
type BatBuoc<T> = { [K in keyof T]-?: T[K] }; // BO ? (bat buoc lai) - ▸readonly [K in keyof T] thêm readonly; [K in keyof T]? thêm optional cho mọi field.
- ▸Dấu - gỡ modifier: -readonly bỏ readonly, -? làm field bắt buộc trở lại.
- ▸Đây chính là cách Readonly<T> và Partial<T> có sẵn được dựng nên.
Template literal type dùng đúng cú pháp dấu huyền như template string, nhưng
chạy trên KIỂU: nó ghép các literal thành literal mới. Kèm các kiểu thao tác chuỗi có sẵn như Capitalize:
template-literal.ts
type Handler = `on${Capitalize<"click">}`;
// Handler = "onClick"
type SuKien = "click" | "hover";
type Handlers = `on${Capitalize<SuKien>}`;
// Handlers = "onClick" | "onHover" (phan phoi tren union) Giống cú pháp value, nhưng ở tầng type
Ghép mapped type với template literal qua mệnh đề as, ta đổi tên khoá trong lúc map. Ví dụ kinh điển: sinh ra một getter cho mỗi field:
key-remapping.ts
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
// Getters<Meo> = {
// getTen: () => string;
// getTuoi: () => number;
// } - ▸[K in keyof T as TenMoi] đổi tên khoá K thành TenMoi trong lúc map.
- ▸Ghép với template literal để sinh tên theo mẫu: getTen, getTuoi...
- ▸Map một khoá sang never thì khoá đó bị loại - cách lọc bớt field.
Mapped type duyệt và biến đổi từng khoá; template literal type ghép tên ở tầng kiểu; key remapping đổi tên khoá. Cùng với conditional và infer, đây là bộ đồ nghề đầy đủ để SINH kiểu từ kiểu. Bài sau cho thấy thư viện chuẩn dùng chính bộ đồ nghề này.
Bài tiếp theo: Utility types
Câu hỏi thường gặp
Index signature ({ [k: string]: number }) mô tả khoá ĐỘNG bất kỳ - bạn không biết trước có những khoá nào. Mapped type ({ [K in keyof T]: ... }) thì DUYỆT qua đúng các khoá CỦA một kiểu đã có (T), để biến đổi từng khoá đó thành kiểu mới.
keyof T cho ra union các khoá của T. Với Meo = { ten: string; tuoi: number } thì keyof Meo là "ten" | "tuoi". Mapped type dùng [K in keyof T] để lần lượt lấy từng khoá đó ra xử lý.
Template string (xin chao ${ten}) ghép CHUỖI lúc chạy. Template literal TYPE (on${Cap}) ghép ở tầng KIỂU lúc biên dịch, tạo ra các literal type cụ thể (vd "onClick"). Cú pháp giống nhau nhưng một cái làm trên giá trị, một cái làm trên kiểu.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Mapped type { [K in keyof T]: ... } làm gì?
- 1
Đọc mapped type
Tự viết type Sao<T> = { [K in keyof T]: T[K] } và áp lên một interface có 2-3 field. Dùng widget Bước 1 hoặc Playground xem kết quả.
✅ Hoàn thành khi: Kết quả là bản sao đúng các field của kiểu gốc; ghi lại keyof của kiểu đó.
- 2
Thêm readonly và optional
Viết hai mapped type: một thêm readonly cho mọi field, một thêm ? cho mọi field. Thử gán lại field readonly và bỏ trống field optional.
✅ Hoàn thành khi: readonly chặn gán lại (TS2540); optional cho phép bỏ trống; ghi lại shape kết quả của mỗi cái.
- 3
Gỡ modifier
Viết một mapped type dùng -readonly và -? để GỠ readonly và optional khỏi một kiểu đã readonly + optional.
✅ Hoàn thành khi: Kết quả trở lại field thường (không readonly, không ?); kèm một câu giải thích dấu - làm gì.
- 4
Ghép tên bằng template literal
Viết type Handler =
on${Capitalize<"click">}và một biến thể áp lên một union các sự kiện.✅ Hoàn thành khi: Handler ra "onClick"; bản union ra một union các tên (vd "onClick" | "onHover").
- 5
Sinh getter bằng key remapping
Viết một mapped type biến mỗi field thành một getter: { [K in keyof T as
get...]: () => T[K] }.✅ Hoàn thành khi: Áp lên một kiểu có field ten/tuoi cho ra getTen: () => string và getTuoi: () => number.
- 6
Lọc khoá bằng never
Viết một mapped type loại bỏ một số khoá bằng cách as ... sang never (gợi ý: dùng conditional trong phần as).
✅ Hoàn thành khi: Kết quả là kiểu chỉ còn các khoá mong muốn; giải thích vì sao as never làm khoá biến mất.