Nếu bạn từng phải khai báo hai, ba interface gần giống hệt nhau chỉ vì một cái cần mọi trường là tùy chọn, cái kia cần bỏ bớt một trường, thì utility types chính là thứ bạn đang tìm. TypeScript cung cấp sẵn một bộ kiểu "biến đổi kiểu" giúp tạo kiểu mới từ kiểu có sẵn. Khi kiểu gốc thay đổi, các kiểu dẫn xuất tự cập nhật theo, nhờ đó codebase ít lặp lại và khó lệch pha hơn. Bài viết này giới thiệu 10 utility type mà đội ngũ Besoft dùng gần như mỗi ngày.
Để dễ theo dõi, các ví dụ bên dưới dùng chung một kiểu User:
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
avatarUrl?: string;
}Nhóm biến đổi thuộc tính
1. Partial<T>
Partial<T> biến mọi thuộc tính của T thành tùy chọn. Trường hợp dùng điển hình là hàm cập nhật dữ liệu, nơi người gọi chỉ gửi lên những trường thay đổi.
function updateUser(id: number, patch: Partial<User>) {
// patch có thể chỉ chứa { name } hoặc { email, role }
}2. Required<T>
Ngược với Partial, Required<T> bắt buộc mọi thuộc tính phải có mặt, kể cả những trường vốn được khai báo với dấu ?. Kiểu này hữu ích khi bạn đã điền giá trị mặc định cho cấu hình và muốn phần code phía sau không phải kiểm tra undefined nữa.
3. Readonly<T>
Readonly<T> đánh dấu mọi thuộc tính là chỉ đọc. Nó rất hợp với state trong các thư viện quản lý trạng thái, hoặc với object cấu hình mà bạn không muốn ai vô tình gán lại. Lưu ý rằng Readonly chỉ tác động ở cấp đầu tiên, các object lồng bên trong vẫn có thể bị sửa.
Nhóm chọn và loại bỏ thuộc tính
4. Pick<T, K>
Pick tạo kiểu mới chỉ gồm các khóa bạn chọn. Đây là cách gọn gàng để mô tả dữ liệu hiển thị trên một thẻ thông tin nhỏ:
type UserCard = Pick<User, "name" | "avatarUrl">;5. Omit<T, K>
Omit làm điều ngược lại: giữ tất cả trừ những khóa được liệt kê. Ví dụ phổ biến nhất là payload tạo mới, khi id do database sinh ra:
type CreateUserInput = Omit<User, "id">;6. Record<K, T>
Record<K, T> mô tả một object có tập khóa K và giá trị kiểu T. Khi K là một union, TypeScript sẽ nhắc bạn nếu quên khai báo một khóa nào đó, rất tiện cho bảng tra cứu:
const roleLabel: Record<User["role"], string> = {
admin: "Quản trị viên",
editor: "Biên tập viên",
viewer: "Người xem",
};Nhóm làm việc với union
7. Exclude<T, U> và Extract<T, U>
Hai kiểu này lọc các thành viên của một union. Exclude bỏ đi những thành viên gán được cho U, còn Extract thì chỉ giữ lại chúng. Chẳng hạn Exclude<User["role"], "viewer"> cho ra "admin" | "editor", tức nhóm người có quyền chỉnh sửa.
8. NonNullable<T>
NonNullable<T> loại bỏ null và undefined khỏi kiểu. Sau khi đã kiểm tra dữ liệu tồn tại, bạn có thể dùng nó để khai báo kiểu cho các bước xử lý tiếp theo mà không phải rải dấu ! khắp nơi.
Nhóm làm việc với hàm và Promise
9. ReturnType<T> và Parameters<T>
ReturnType lấy kiểu trả về của một hàm, Parameters lấy kiểu các tham số dưới dạng tuple. Chúng đặc biệt hữu ích khi làm việc với thư viện bên thứ ba không export sẵn kiểu, hoặc khi bạn muốn kiểu dữ liệu luôn bám sát một hàm factory:
function createStore() {
return { items: [] as string[], add(item: string) {} };
}
type Store = ReturnType<typeof createStore>;10. Awaited<T>
Awaited<T> "mở" kiểu Promise để lấy giá trị bên trong, kể cả khi Promise lồng nhiều lớp. Kết hợp với ReturnType, bạn lấy được kiểu dữ liệu thực tế của một hàm async:
async function fetchUser(): Promise<User> { /* ... */ }
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>; // UserKết hợp utility types trong thực tế
Sức mạnh thật sự đến từ việc ghép các kiểu lại với nhau. Ví dụ, form chỉnh sửa hồ sơ cho phép sửa tên và avatar nhưng không được đổi id hay role:
type ProfileForm = Partial<Pick<User, "name" | "avatarUrl">>;Một vài lời khuyên khi áp dụng:
- Đặt tên cho kiểu dẫn xuất thay vì viết lồng nhau trực tiếp trong chữ ký hàm, để thông báo lỗi dễ đọc hơn.
- Đừng lồng quá ba lớp utility type trong một biểu thức. Nếu cần nhiều hơn, hãy tách thành các kiểu trung gian có tên rõ nghĩa.
- Ưu tiên dẫn xuất từ một nguồn sự thật duy nhất (như kiểu sinh ra từ schema database hoặc từ API) thay vì viết tay nhiều interface song song.
- Nhớ rằng
Omitkhông báo lỗi khi bạn gõ sai tên khóa, vì tham số thứ hai chấp nhận mọi chuỗi. Hãy kiểm tra kỹ hoặc tự viết phiên bản chặt hơn nếu cần.
Kết luận
Utility types là những công cụ nhỏ nhưng mang lại lợi ích lớn: ít code lặp lại hơn, kiểu dữ liệu tự đồng bộ khi model thay đổi và ý định của lập trình viên được thể hiện rõ ngay trong chữ ký hàm. Bạn không cần thuộc lòng toàn bộ danh sách. Chỉ cần nắm vững khoảng mười kiểu trên là đã xử lý được phần lớn tình huống thường gặp. Khi đã quen, hãy tìm hiểu thêm mapped types và conditional types, vốn là nền tảng để chính các utility type này được xây dựng.






