0

Series Total TypeScript Thực chiến #4: Mapped Types & Key Remapping – Vòng lặp "Biến hình" Object

1. Mapped Types là gì? (Vòng lặp của Type)

Hãy tưởng tượng bạn có một Model đại diện cho thiết bị trạm vé (TVM - Ticket Vending Machine) trong hệ thống AFC:

type TicketMachine = {
  id: string;
  ipAddress: string;
  isOnline: boolean;
  paperRollCount: number;
};

Bây giờ, bạn muốn tạo ra một Type cấu hình để bật/tắt quyền chỉnh sửa cho từng trường dữ liệu trên. Tức là một Object có cùng các keys như TicketMachine, nhưng tất cả values đều là boolean.

Thay vì copy-paste và sửa tay từng dòng, bạn dùng Mapped Types:

// 🔥 Dùng vòng lặp [K in keyof T]
type MachineConfig = {
  [Key in keyof TicketMachine]: boolean;
};

/*
Lúc này, MachineConfig tự động trở thành:
{
  id: boolean;
  ipAddress: boolean;
  isOnline: boolean;
  paperRollCount: boolean;
}
*/

Cú pháp [Key in keyof TicketMachine] hoạt động y hệt một vòng lặp for (let key in object). Nó lặp qua từng "chìa khóa" của TicketMachine, và gán cho nó một "giá trị" Type mới (ở đây là boolean).

2. Truy xuất Type gốc trong vòng lặp

Sẽ thế nào nếu bạn không muốn gán cứng một Type như boolean, mà muốn giữ nguyên Type gốc, hoặc bọc Type gốc vào một mảng? Bạn có thể sử dụng lại Indexed Access (T[K]) mà chúng ta đã học ở Bài 2 ngay bên trong vòng lặp.

Ví dụ, tạo một Type để hứng dữ liệu update hàng loạt từ Client, trong đó mỗi trường đều là một mảng các giá trị:

type BulkUpdatePayload = {
  [K in keyof TicketMachine]: TicketMachine[K][]; // Lấy type gốc và bọc vào Array
};

/*
Kết quả:
{
  id: string[];
  ipAddress: string[];
  isOnline: boolean[];
  paperRollCount: number[];
}
*/

3. Tuyệt kỹ Key Remapping: Đổi tên Key với từ khóa as

Đến TypeScript 4.1, Microsoft tung ra một tính năng làm chao đảo giới lập trình viên: Key Remapping. Nó cho phép bạn kết hợp vòng lặp Mapped Types với Template Literal Types (Bài 3) để đổi tên (rename) các keys ngay trong lúc đang lặp.

Hãy giải quyết một bài toán kinh điển: Tự động sinh ra các hàm Setter cho model TicketMachine ở trên. Yêu cầu: id -> setId, ipAddress -> setIpAddress...

Cú pháp sử dụng từ khóa as:

type MachineSetters = {
  // Lặp qua từng Key -> Đổi tên Key đó (viết hoa chữ đầu, thêm 'set') -> Gán giá trị là 1 function
  [K in keyof TicketMachine as `set${Capitalize<K>}`]: (value: TicketMachine[K]) => void;
};

/*
🔥 Cấu hình MachineSetters sinh ra chính xác như sau:
{
  setId: (value: string) => void;
  setIpAddress: (value: string) => void;
  setIsOnline: (value: boolean) => void;
  setPaperRollCount: (value: number) => void;
}
*/

4. Ứng dụng thực chiến: Lọc bỏ trường dữ liệu (Filtering Keys)

Không chỉ đổi tên, Key Remapping kết hợp với as còn giúp bạn xóa bỏ các trường không mong muốn khỏi một Object bằng cách ép nó thành type never.

Giả sử bạn muốn tạo một API trả về thông tin máy bán vé, nhưng bạn muốn tự động lọc bỏ tất cả các trường có type là boolean (vì không muốn lộ trạng thái nội bộ).

Chúng ta sẽ kết hợp một chút với Conditional Types (Kiểu điều kiện extends ? 😃:

type ExcludeBooleans<T> = {
  // Nếu Type gốc (T[K]) là boolean, thì trả về 'never' (xóa key đó đi)
  // Nếu không phải boolean, giữ nguyên key (K)
  [K in keyof T as T[K] extends boolean ? never : K]: T[K];
};

type PublicMachineData = ExcludeBooleans<TicketMachine>;

/*
Kết quả PublicMachineData đã tự động loại bỏ 'isOnline':
{
  id: string;
  ipAddress: string;
  paperRollCount: number;
}
*/

Sức mạnh của Mapped Types và Key Remapping chính là lý do khiến TypeScript vượt trội hoàn toàn so với việc chỉ dùng JSDoc hay các ngôn ngữ định kiểu tĩnh khác. Nó cho phép mã nguồn của chúng ta có tính linh hoạt cực cao, tự động thích ứng với mọi sự thay đổi của Database Schema hay Business Logic. Hệ thống Type lúc này không còn là gánh nặng phải bảo trì, mà đóng vai trò như một người đồng nghiệp thầm lặng, tự động cập nhật mọi DTO và Interface mỗi khi bạn thay đổi cấu trúc dữ liệu lõi!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí