DEV Community

Utility Type — Partial Pick Omit

Utility type: tạo kiểu phái sinh thay vì khai báo lặp

Một entity và các biến thể của nó — kiểu đầy đủ, kiểu để tạo mới (chưa có id), kiểu để cập nhật (mọi field optional), kiểu trả về public (ẩn password) — thường được khai báo riêng từng cái, lặp lại các field và lệch nhau khi entity đổi. Utility type (Partial, Required, Pick, Omit, Readonly, Record) tạo các kiểu đó phái sinh từ một nguồn duy nhất, nên khi entity gốc đổi, mọi biến thể tự cập nhật theo. Đây là cách giữ DTO đồng bộ với model mà không sao chép thủ công.

Cơ chế hoạt động

Utility type là các generic dựng sẵn biến đổi một kiểu thành kiểu khác:

interface User { id: string; name: string; email: string; password: string }

type CreateUser = Omit<User, 'id'>          // mọi field trừ id
type UpdateUser = Partial<Omit<User, 'id'>> // mọi field optional, trừ id
type PublicUser = Omit<User, 'password'>    // ẩn password khi trả ra ngoài
type UserKeys = Pick<User, 'id' | 'name'>   // chỉ lấy id và name
Enter fullscreen mode Exit fullscreen mode

Partial<T> biến mọi field thành optional; Required<T> ngược lại; Pick<T, K> giữ một tập field; Omit<T, K> bỏ một tập field; Readonly<T> khóa ghi. Điểm cốt lõi: User là nguồn sự thật duy nhất — thêm một field vào User, tất cả các kiểu phái sinh tự có (hoặc tự loại) field đó, không cần sửa từng chỗ.

Vấn đề gặp trong production

Use case điển hình: DTO cho từng tầng. Cùng một entity cần hình dạng khác nhau ở các ranh giới — request body khi tạo (không có id, không cho client set), response trả ra (ẩn field nhạy cảm), payload cập nhật (optional):

function createUser(input: Omit<User, 'id'>): User { /* ... */ }
function updateUser(id: string, patch: Partial<Omit<User, 'id'>>): User { /* ... */ }
function toPublic(u: User): Omit<User, 'password'> { const { password, ...rest } = u; return rest }
Enter fullscreen mode Exit fullscreen mode

Không có utility type, mỗi DTO là một interface viết tay; khi User thêm field phone, phải nhớ sửa cả CreateUser, UpdateUser, PublicUser — bỏ sót một chỗ là một bug âm thầm (field mới không được nhận khi tạo, hoặc password rò ra response).

Failure mode: type explosion. Lồng quá nhiều utility type vào nhau tạo ra kiểu khó đọc và thông báo lỗi dài: Partial<Pick<Omit<User, 'id'>, 'name' | 'email'>>. Khi một kiểu cần ba bốn lớp biến đổi, thường nên đặt tên trung gian rõ ràng (type EditableFields = ...) hoặc xem lại liệu model có nên tách ngay từ đầu. Utility type giảm lặp nhưng không nên trở thành câu đố.

Failure mode: Omit không bắt key sai trên union. Omit không kiểm tra key truyền vào có tồn tại không trong một số trường hợp với union type, nên Omit<User, 'passwrod'> (typo) có thể không báo lỗi và âm thầm không bỏ gì cả — password vẫn lọt ra. Khi dùng Omit cho mục đích bảo mật (ẩn field), kiểm tra kỹ hoặc bọc thêm một type-check rằng field thật sự bị loại.

Cách debug và monitor

Khi một DTO "thiếu field mới thêm" hoặc "lộ field đáng lẽ ẩn", kiểm tra nó có phái sinh từ entity gốc qua utility type không — nếu là interface viết tay riêng thì gần như chắc đã lệch khỏi model. Hover kiểu phái sinh trong editor để xem TS giải nó thành hình dạng gì thực tế, xác nhận đúng field. Với kiểu để bảo mật (ẩn password/token), viết một test kiểu hoặc test runtime khẳng định field nhạy cảm không có trong response — không tin tuyệt đối vào Omit vì nó có chỗ không bắt key sai. Lồng utility type quá ba lớp là tín hiệu nên đặt tên trung gian.

Tradeoff

Utility type giảm trùng lặp và giữ các biến thể của một kiểu đồng bộ với nguồn — sửa entity một chỗ, mọi DTO tự theo, ít cơ hội lệch và sót. Cái giá là kiểu phái sinh trừu tượng hơn (phải giải mã Omit<Partial<...>> để biết hình dạng thật) và lồng sâu thì khó đọc, lỗi dài. Quy tắc thực tế: một entity gốc làm nguồn sự thật, các DTO phái sinh bằng Pick/Omit/Partial; đặt tên trung gian khi cần nhiều lớp; và với kiểu dùng cho bảo mật, xác minh thêm chứ không dựa hoàn toàn vào Omit.

Câu hỏi phỏng vấn

Khi nào dùng Partial, Pick, Omit, và lợi ích chính của utility type là gì?

Partial<T> biến mọi field thành optional — hợp cho payload cập nhật (PATCH) khi client chỉ gửi field muốn đổi; Pick<T, K> giữ một tập field — hợp khi cần một subset (ví dụ kiểu cho dropdown chỉ cần id + name); Omit<T, K> bỏ một tập field — hợp cho DTO tạo mới (bỏ id) hoặc response public (ẩn password). Lợi ích chính là tạo kiểu phái sinh từ một entity gốc duy nhất thay vì khai báo lặp: khi entity đổi, mọi biến thể tự cập nhật, tránh lệch và sót field. Điểm ăn điểm: cảnh báo type explosion khi lồng quá nhiều lớp (đặt tên trung gian), và lưu ý Omit có trường hợp không bắt key sai nên với mục đích ẩn field nhạy cảm cần xác minh thêm bằng test.

Hands-on

Lấy một entity thật (User hoặc Order với field nhạy cảm như password/token) làm nguồn sự thật, rồi phái sinh các DTO bằng utility type: CreateXxx = Omit<…, 'id'>, UpdateXxx = Partial<Omit<…, 'id'>>, PublicXxx = Omit<…, 'password'>. Thêm một field mới vào entity gốc và xác nhận các DTO tự cập nhật, không phải sửa tay. Viết một test khẳng định response public không chứa field nhạy cảm, rồi cố tình gõ sai key trong Omit để kiểm tra TS có bắt không — quan sát trường hợp nó không bắt và thêm lớp xác minh. Cuối cùng tạo một kiểu lồng ba lớp utility type, thấy lỗi/đọc khó thế nào, và refactor bằng cách đặt tên trung gian.

Top comments (0)