Mục lục
Hầu hết chúng ta bắt đầu với TypeScript bằng cách viết
interface User { name: string; age: number; }. Cách này tuyệt vời, nhưng nếu dừng lại ở đó,
bạn mới chỉ khai thác được 20% sức mạnh của TypeScript.
Khi bạn bắt đầu viết các thư viện (libraries) dùng chung, hoặc xây dựng kiến trúc lõi cho một dự án lớn, việc hardcode các Interface tĩnh là không đủ. Bạn cần hệ thống Type phải biết "lập trình", biết suy luận dựa trên đầu vào giống hệt như một hàm JavaScript thực thụ. Chào mừng bạn đến với thế giới của Mapped Types và Conditional Types.
Vượt Qua Mức Cơ Bản
Hãy tưởng tượng bạn có một User type. Đột nhiên sếp yêu cầu viết một tính năng cập nhật
User, trong đó mọi trường đều có thể bỏ trống (Optional), và một tính năng khác yêu cầu không ai được sửa
dữ liệu (Readonly).
Thay vì viết tay ba Interface khác nhau (User, OptionalUser,
ReadonlyUser), TypeScript cho phép bạn viết các "Hàm Biến Đổi Type" (Utility Types). Để làm
được điều này, chúng ta cần học cách dùng vòng lặp và câu lệnh điều kiện ngay bên trong hệ thống Type.
Mapped Types (Vòng lặp của Type)
Mapped Types chính là vòng lặp for...in hoặc Array.map() trong
thế giới của Type. Nó giúp bạn duyệt qua tất cả các "key" của một Type cũ và biến đổi chúng thành một Type
mới.
Cú pháp cốt lõi: [K in keyof T].
type User = {
name: string;
age: number;
isAdmin: boolean;
};
// Vòng lặp: Duyệt qua tất cả các khóa của T, ép tất cả thành boolean
type Booleanify<T> = {
[K in keyof T]: boolean;
};
type UserFlags = Booleanify<User>;
// Kết quả: { name: boolean; age: boolean; isAdmin: boolean; }
Đỉnh cao của Mapped Types là việc thêm (hoặc xóa) các modifiers như readonly hay dấu
? (optional).
// Đây chính là cách TypeScript tạo ra Partial<T> mặc định
type MyPartial<T> = {
[K in keyof T]?: T[K]; // Dấu ? biến thuộc tính thành optional
};
// Bạn cũng có thể xóa thuộc tính bằng dấu trừ (-)
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // Dấu -? xóa sự optional
};
Conditional Types (If/Else của Type)
Conditional Types đóng vai trò như lệnh if/else hay toán tử ba ngôi
(ternary operator) đối với Type.
Cú pháp: T extends U ? X : Y (Nếu T là một tập con của U thì trả về type X, ngược lại trả về
type Y).
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // Kết quả: true
type B = IsString<123>; // Kết quả: false
Điều này nghe có vẻ vô dụng nếu dùng đơn lẻ, nhưng khi kết hợp với Generic, nó mang lại khả năng phân luồng Type siêu hạng. Bạn có thể định nghĩa một API trả về kiểu dữ liệu khác nhau tùy thuộc vào input người dùng đưa vào.
Sức Mạnh Kết Hợp (Filter/Omit)
Bây giờ hãy kết hợp Mapped Types (Vòng lặp) và Conditional Types (If/Else). Giả sử bạn muốn viết một Type
tự động loại bỏ tất cả các thuộc tính kiểu Function (phương thức) ra khỏi một Object, chỉ giữ
lại thuộc tính dữ liệu thuần.
type Person = {
name: string;
age: number;
speak(): void; // Muốn xóa dòng này
walk(): void; // Muốn xóa dòng này
};
// Bước 1: Lấy ra các key KHÔNG PHẢI là Function
type NonFunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? never : K
}[keyof T];
// NonFunctionKeys<Person> trả về "name" | "age"
// Bước 2: Bốc các key đó ra thành Object mới (Pick)
type PureData<T> = Pick<T, NonFunctionKeys<T>>;
type PersonData = PureData<Person>;
// Kết quả: { name: string; age: number; }
Lưu ý kỹ thuật never: Trong TypeScript, never đại diện cho tập hợp rỗng. Khi
một union type kết hợp với never (ví dụ: "name" | never), nó sẽ tự động triệt
tiêu never đi và chỉ còn lại "name". Đây là cốt lõi của kỹ thuật Filter Type.
Đỉnh Cao: Từ Khóa "infer"
Bí thuật cuối cùng của Conditional Types là từ khóa infer (suy luận). Nó cho phép bạn "bốc"
một type nhỏ nằm ẩn sâu bên trong một type lớn ra ngoài.
Ví dụ kinh điển: Làm sao để lấy kiểu dữ liệu trả về (Return Type) của một Function Type?
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type ThoiTietFn = () => { nhietDo: number; troiMua: boolean };
// Sử dụng infer để trích xuất cái đống bùi nhùi đằng sau mũi tên (=>)
type ThoiTietData = MyReturnType<ThoiTietFn>;
// Kết quả: { nhietDo: number; troiMua: boolean }
"Nếu T là một Hàm, tôi không quan tâm tham số của nó là gì (...args: any[]), tôi chỉ muốn
ông (TypeScript) tự động SUY LUẬN (infer) kiểu dữ liệu đầu ra và nhét nó vào biến R, sau đó
trả về R cho tôi."
infer cực kỳ mạnh khi bạn làm việc với Redux, React Query, hoặc lấy kiểu dữ liệu bên trong
một mảng (Array) hay một Promise.
Tổng Kết
Khi bạn hiểu và làm chủ được Mapped Types (lặp) và Conditional Types (điều kiện), TypeScript sẽ không còn là một công cụ chỉ để kiểm tra tĩnh (static checking) nữa. Nó trở thành một ngôn ngữ lập trình Meta (Meta-programming).
Bạn có thể viết các Utility Types để bảo vệ code chặt chẽ tới mức một lập trình viên khác trong team gõ sai một chữ cái cũng không thể Compile nổi. Đó chính là cảnh giới Type-Safe 100% của các Library Author. Happy Coding!