Omit<T, K> là một utility type có sẵn, tạo ra kiểu object có mọi thuộc tính của T trừ các khóa được liệt kê trong K. Cách dùng điển hình: một kiểu User có password, và một phiên bản công khai không có nó.
PublicUser luôn đồng bộ với User: thêm một trường vào User thì nó cũng xuất hiện trong PublicUser, còn password vẫn bị loại ra. Omit chỉ tác động lên kiểu; chính phép destructuring trong toPublic mới xóa thuộc tính khỏi object thật.
Bỏ nhiều khóa
Đối số thứ hai là một union, nên bạn liệt kê bao nhiêu khóa tùy ý bằng |:
"Kiểu không có các trường do máy chủ tạo ra" là cách dùng Omit phổ biến nhất trong code thực tế: một kiểu model duy nhất, với Omit suy ra cấu trúc đầu vào cho form tạo mới và request API.
Omit được định nghĩa thế nào
Omit được xây từ hai utility type khác:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Đọc từ trong ra ngoài: keyof T là union của mọi khóa, Exclude<keyof T, K> loại các khóa không mong muốn khỏi union đó, và Pick giữ lại các khóa còn lại. Vậy Omit<User, "password"> chính là Pick<User, "id" | "name" | "email">. Vì Pick ánh xạ qua các khóa của T, các modifier readonly và ? trên những thuộc tính còn lại vẫn được giữ.
Hãy để ý ràng buộc: K extends keyof any, không phải K extends keyof T. Chi tiết này là chủ đề của phần tiếp theo.
Omit không kiểm tra khóa có tồn tại hay không
keyof any là string | number | symbol, nên Omit nhận bất kỳ khóa nào, kể cả khóa mà T không có. Một lỗi gõ nhầm vẫn biên dịch và không loại bỏ gì:
Không có lỗi nào cả, và mật khẩu bị gửi ra ngoài trong JSON. Ràng buộc lỏng này được chọn có chủ ý khi Omit được thêm vào TypeScript 3.5. Lý do của nhóm phát triển: trong code generic, các khóa bị bỏ thường không đến từ T (một hàm gộp hai object có thể khai báo đối số rest là Omit<T1, keyof T2>), Exclude cũng không ràng buộc tham số thứ hai của nó, và một phiên bản có ràng buộc sẽ làm hỏng nhiều kiểu Omit tự viết mà các dự án đã có. Với code hằng ngày, một phiên bản chặt chẽ hơn sẽ bắt được lỗi gõ nhầm:
index.ts(10,36): error TS2344: Type '"pasword"' does not satisfy the constraint 'keyof User'.
index.ts(11,7): error TS2741: Property 'password' is missing in type '{ id: number; name: string; }' but required in type 'PublicUser'.
Lỗi đầu tiên mới là lỗi quan trọng: khóa được kiểm tra với keyof User. Lỗi thứ hai là hệ quả, vì không có gì bị bỏ và PublicUser vẫn yêu cầu password. Sửa chính tả thành "password" là biên dịch được. Nhiều dự án định nghĩa nó một lần trong tệp kiểu dùng chung và dùng thay cho Omit.
Ghi đè kiểu của thuộc tính
Một interface không thể kế thừa interface khác rồi đổi một thuộc tính sang kiểu không tương thích: interface ApiUser extends User { id: string } thất bại với error TS2430: Interface 'ApiUser' incorrectly extends interface 'User'. Hãy bỏ thuộc tính đó trước, rồi khai báo lại:
Với type alias, việc tương tự là một intersection: type ApiUser = Omit<User, "id"> & { id: string }. Nếu không có Omit, intersection User & { id: string } sẽ cho id kiểu number & string, tức là never.
Omit trên kiểu union
Omit không có tính phân phối. Trên một union, nó bắt đầu từ keyof của cả union, vốn chỉ chứa các khóa mà mọi thành phần đều có, nên thuộc tính riêng của từng thành phần biến mất:
index.ts(8,39): error TS2353: Object literal may only specify known properties, and 'radius' does not exist in type 'NewShape'.
NewShape trở thành { kind: "circle" | "square" }. Để bỏ thuộc tính khỏi từng thành phần riêng, áp dụng Omit bên trong một conditional type có tính phân phối:
T extends unknown ? ... : never luôn đúng; nó chỉ có mặt để conditional type phân phối qua union. Trang conditional types giải thích vì sao cách này hoạt động. Hãy dùng DistributiveOmit mỗi khi kiểu bạn bỏ thuộc tính là một discriminated union.
Bỏ một thuộc tính lồng nhau
Omit chỉ xét các khóa ở cấp ngoài cùng, nên Omit<Settings, "editor.wordWrap"> không loại bỏ gì (và như đã thấy ở trên, cũng không báo lỗi). Để bỏ một thuộc tính ở cấp sâu hơn, hãy bỏ thuộc tính cha rồi thêm lại nó với Omit riêng:
Settings["editor"] là một indexed access type: nó đặt tên cho kiểu object lồng bên trong để Omit có thể xử lý.
Xóa thuộc tính lúc chạy
Omit chỉ thay đổi kiểu. Để tạo ra object không có các thuộc tính đó, hãy loại chúng bằng destructuring, hoặc viết một hàm generic nhỏ có kiểu trả về là Omit:
Hàm này ràng buộc K theo keyof T, nên khóa viết sai chính tả sẽ báo lỗi ngay tại chỗ gọi. Bản sao chỉ ở mức nông: các object lồng bên trong vẫn dùng chung với bản gốc.
So sánh Omit, Pick và Exclude
| Áp dụng cho | Giữ lại | Ví dụ | Kết quả | |
|---|---|---|---|---|
Omit<T, K> | kiểu object | mọi thuộc tính trừ K | Omit<User, "password"> | { id; name; email } |
Pick<T, K> | kiểu object | chỉ các thuộc tính trong K | Pick<User, "id" | "name"> | { id; name } |
Exclude<U, M> | kiểu union | các thành phần union không gán được cho M | Exclude<"a" | "b" | "c", "a"> | "b" | "c" |
Chọn giữa Omit và Pick dựa trên điều bạn muốn xảy ra khi kiểu nguồn có thêm thuộc tính. Omit tự động đưa thuộc tính mới vào, hợp với trường hợp "mọi thứ trừ phần bí mật". Pick không bao giờ đưa chúng vào, hợp với trường hợp "đúng những trường này" và an toàn hơn cho mọi dữ liệu rời khỏi máy chủ của bạn. Pick kiểm tra khóa với keyof T; Omit thì không.
Câu hỏi thường gặp
Omit trong TypeScript dùng để làm gì?
Omit<T, K> tạo một kiểu object mới có tất cả thuộc tính của T trừ những thuộc tính có khóa nằm trong K. Omit<User, "password"> là User không có password. Nó chỉ thay đổi kiểu; nó không xóa gì khỏi object lúc chạy.
Làm sao bỏ nhiều thuộc tính cùng lúc trong TypeScript?
Truyền một union các khóa làm đối số thứ hai: Omit<User, "password" | "createdAt" | "updatedAt">. Mọi khóa trong union đều bị loại bỏ.
Omit và Exclude khác nhau thế nào?
Omit loại bỏ thuộc tính khỏi một kiểu object: Omit<{ a: 1; b: 2 }, "a"> là { b: 2 }. Exclude loại bỏ thành phần khỏi một kiểu union: Exclude<"a" | "b", "a"> là "b". Omit được cài đặt bằng Exclude: Pick<T, Exclude<keyof T, K>>.
Vì sao Omit không báo lỗi với khóa không tồn tại?
Tham số khóa của nó bị ràng buộc theo keyof any (mọi string, number hoặc symbol), không phải keyof T, nên một lỗi gõ nhầm như Omit<User, "pasword"> vẫn biên dịch và không loại bỏ gì. Hãy định nghĩa type StrictOmit<T, K extends keyof T> = Omit<T, K> để nhận lỗi với khóa không có.
Làm sao xóa một thuộc tính khỏi object lúc chạy trong TypeScript?
Dùng destructuring với phần tử rest: const { password, ...rest } = user;. TypeScript gán cho rest kiểu object không có password, cùng cấu trúc với Omit<User, "password">, và thuộc tính đó thực sự không còn trong object mới.