Bẻ Khóa TypeScript: Mapped Types & Conditional Types

Làm chủ các khái niệm nâng cao trong TypeScript để xây dựng các thư viện an toàn kiểu dữ liệu (type-safe) 100%.

14/05/2026
14 phút đọ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 TypesConditional Types.

1

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.

2

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
};
3

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.

4

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.

5

Đỉ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 }
Cách đọc câu lệnh trên:

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

6

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!

Bài viết liên quan

Micro Frontends Module Federation Với Vite

Chia nhỏ ứng dụng khổng lồ thành các micro frontend độc lập với tốc độ build chớp nhoáng của Vite.

14/05/2026
12 phút đọc