Series Total TypeScript Thực chiến #5: Conditional Types và "Phép thuật" infer – Trinh sát Type
1. Conditional Types: Lệnh if/else trong thế giới Type
Cú pháp của Conditional Types hoàn toàn giống với toán tử ba ngôi (Ternary Operator) trong JavaScript: T extends U ? X : Y (Nếu Type T có cấu trúc thỏa mãn Type U, thì trả về Type X, ngược lại trả về Type Y).
Ví dụ cơ bản nhất:
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<"Hello">; // Kết quả: true
type Test2 = IsString<123>; // Kết quả: false
Tuy nhiên, sức mạnh thực sự của nó nằm ở việc kết hợp với các cấu trúc lồng nhau. Hãy xem cách chúng ta định nghĩa một Type tự động quyết định cấu trúc Response của một API dựa trên trạng thái thành công hay thất bại:
// Nếu T là true, trả về cục Data. Nếu false, trả về cục Error.
type ApiResponse<T extends boolean> = T extends true
? { status: 200; data: object }
: { status: 500; errorMessage: string };
type Success = ApiResponse<true>; // { status: 200; data: object }
type Fail = ApiResponse<false>; // { status: 500; errorMessage: string }
2. Sự xuất hiện của infer: "Khai báo biến" ngay trong điều kiện
Giả sử bạn có một Type là một Mảng (string[]). Bạn muốn lấy ra cái Type nằm bên trong mảng đó (string). Bằng cách nào?
Bạn có thể viết một điều kiện: "Nếu T là một mảng của CÁI GÌ ĐÓ, hãy trả về CÁI GÌ ĐÓ". Từ khóa infer (suy luận/nội suy) sinh ra để đại diện cho chữ "CÁI GÌ ĐÓ".
Nó đóng vai trò như việc bạn khai báo một biến tạm thời ngay bên trong mệnh đề extends để "hứng" lấy Type bị giấu bên trong.
type UnwrapArray<T> = T extends Array<infer ItemType> ? ItemType : T;
// Cách hoạt động:
// TypeScript thấy Array<string>. Nó so sánh với Array<infer ItemType>.
// Khớp! Nó tự động gán ItemType = string. Sau đó trả về ItemType.
type A = UnwrapArray<string[]>; // Kết quả: string
type B = UnwrapArray<number[]>; // Kết quả: number
type C = UnwrapArray<boolean>; // Kết quả: boolean (Vì boolean không extends Array, nó nhảy vào nhánh 'else' trả về chính T)
3. Thực chiến: Bóc tách Kiểu trả về của một Hàm (Mô phỏng ReturnType)
Khi sử dụng các thư viện bên thứ 3 trong Node.js, rất nhiều khi tác giả thư viện export ra một hàm (Function), nhưng lại... lười không export cái Type kết quả trả về của hàm đó.
Làm sao để lấy được Type kết quả để dùng cho các biến nội bộ của mình? Hãy dùng infer để "bắt cóc" nó!
// Một hàm từ thư viện bên ngoài không export Type kết quả
const fetchUserData = () => {
return { id: 1, username: "admin", role: "super_admin" };
};
// Tự xây dựng công cụ trích xuất:
type MyReturnType<T> = T extends (...args: any[]) => infer ReturnValue ? ReturnValue : never;
// Sử dụng công cụ:
type UserDataType = MyReturnType<typeof fetchUserData>;
/*
Kết quả UserDataType hoàn hảo:
{
id: number;
username: string;
role: string;
}
*/
Lưu ý: Bạn không cần phải tự viết MyReturnType, TypeScript đã tích hợp sẵn nó dưới cái tên ReturnType<T>. Nhưng hiểu cách nó hoạt động bên dưới sẽ giúp bạn linh hoạt biến tấu.
4. Thực chiến: Đào sâu vào Promise
Trong Backend Node.js, 99% các thao tác I/O (gọi Database, gọi API) đều trả về Promise. Khi bạn bọc một hàm bằng async, TypeScript tự động bọc kết quả của bạn vào một Promise<T>.
Nếu bạn có một Type là Promise<string>, làm sao để móc được chữ string ra ngoài để định nghĩa cho các hàm đồng bộ (sync) phía sau?
Tiếp tục sử dụng infer:
type UnwrapPromise<T> = T extends Promise<infer ResolvedType> ? ResolvedType : T;
type Result1 = UnwrapPromise<Promise<number>>; // Kết quả: number
type Result2 = UnwrapPromise<string>; // Kết quả: string
Nhưng sẽ thế nào nếu Promise lồng nhau nhiều cấp như Promise<Promise<string>>? Chúng ta sẽ kết hợp Đệ quy (Recursive) ngay bên trong Conditional Type!
// Cú pháp đệ quy vô cực để bóc trần Promise
type DeepUnwrapPromise<T> = T extends Promise<infer U>
? DeepUnwrapPromise<U> // Nếu bên trong vẫn là Promise, gọi lại chính nó
: T; // Nếu đã tới lõi, trả về lõi
type DeepResult = DeepUnwrapPromise<Promise<Promise<Promise<boolean>>>>;
// Kết quả cuối cùng: boolean
Với Conditional Types và infer, bạn không còn bị giới hạn bởi các bề mặt Type tĩnh nữa. Bạn có thể thả một "con bot trinh sát" (infer) luồn lách vào bên trong các Array, Function, hay Promise phức tạp nhất để đánh cắp bản thiết kế Type và mang về sử dụng.
Đây là công cụ sắc bén nhất để bạn xây dựng các hệ thống Wrapper, Base Controller, hay Base Service trong Backend, nơi dữ liệu truyền vào luôn linh động và thay đổi liên tục!
All rights reserved