← TypeScript Thực Chiến

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

Dấu huyền trong template literal type giống hệt template string của JavaScript mà mèo con đã quen, chỉ khác chỗ nó ghép literal type lúc biên dịch thay vì ghép chuỗi lúc chạy. Và như conditional ở bài trước, nó cũng phân phối qua từng thành viên của một union.

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

Bài sau gặp các utility type có sẵn (Partial, Pick, Omit, Record, ReturnType...) - và mèo con sẽ TỰ DỰNG LẠI chúng từ mapped + conditional, để thấy chúng chỉ là kiểu mình cũng viết được.

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.

Là các kiểu thao tác chuỗi có sẵn (intrinsic): Capitalize<"click"> ra "Click", Uppercase, Lowercase, Uncapitalize tương tự. Chúng chạy ở tầng kiểu, thường đi cùng template literal type để dựng tên.

Khi muốn ĐỔI TÊN khoá trong lúc map, dùng [K in keyof T as TenMoi]. Ghép với template literal là sinh được tên mới theo mẫu (getTen, setTuoi...). Còn map sang never thì khoá đó bị LOẠI - cách lọc bớt field.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Mapped type { [K in keyof T]: ... } làm gì?

Bài tập về nhà

  1. 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. 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. 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. 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. 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. 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.