Exclude<T, U> loại bỏ thành viên khỏi một union type, Extract<T, U> chỉ giữ lại các thành viên khớp, còn NonNullable<T> loại bỏ null và undefined. Cả ba đều lọc union; chúng không đụng đến thuộc tính của đối tượng.
Giống mọi utility type, các kiểu này chỉ tồn tại lúc biên dịch. Dòng cuối bị trình biên dịch từ chối; đoạn JavaScript chạy thật không hề biết Visible là gì.
Tổng quan Exclude, Extract và NonNullable
| Kiểu | Giữ lại | Ví dụ | Kết quả |
|---|---|---|---|
Exclude<T, U> | các thành viên của T không gán được cho U | Exclude<"a" | "b" | "c", "a"> | "b" | "c" |
Extract<T, U> | các thành viên của T gán được cho U | Extract<"a" | "b" | "c", "a" | "z"> | "a" |
NonNullable<T> | mọi thứ trừ null và undefined | NonNullable<string | null | undefined> | string |
"Gán được cho U" là cụm từ then chốt. U không nhất thiết là danh sách các thành viên chính xác; nó có thể là một kiểu rộng hơn mà nhiều thành viên vừa với:
Chọn thành viên của union theo hình dạng
Với một discriminated union, Extract kèm một hình dạng một phần sẽ lấy ra các biến thể có hình dạng đó. Đây là cách thường dùng để đặt tên cho một biến thể của union không do bạn tự viết:
Hình dạng { type: "click" } chỉ là kiểu cha (supertype) của biến thể click, nên đó là biến thể duy nhất Extract giữ lại. Một hình dạng khớp với nhiều biến thể (chẳng hạn { x: number } nếu hai biến thể cùng có) sẽ giữ lại tất cả.
NonNullable
NonNullable<T> là phiên bản ở mức kiểu của phép kiểm tra !== null && !== undefined. Nó hay xuất hiện khi bạn suy ra một kiểu từ thứ gì đó không bắt buộc:
User["manager"] là { name: string } | null | undefined (dấu ? thêm vào undefined), và NonNullable loại bỏ cả hai. Từ TypeScript 5.5, trình biên dịch cũng tự suy ra predicate đó, nên raw.filter((s) => s != null) đã có kiểu string[]; việc viết rõ s is NonNullable<typeof s> giúp ý định dễ thấy và chạy được ở các phiên bản cũ hơn. Định nghĩa của nó là type NonNullable<T> = T & {}: kiểu đối tượng rỗng {} nhận mọi giá trị trừ null và undefined, nên giao với nó sẽ loại bỏ đúng hai giá trị đó.
Chúng được xây dựng thế nào
Exclude và Extract là các conditional type chỉ một dòng trong thư viện chuẩn:
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
Khi T là một union, conditional type được áp dụng riêng cho từng thành viên (nó phân phối), và các thành viên never biến mất khỏi kết quả. Vì vậy Exclude<"a" | "b", "a"> trở thành ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b"), tức là never | "b", tức là "b". Trang conditional type trình bày chi tiết về tính phân phối.
Exclude và Omit, Extract và Pick
Những cái tên này dễ nhầm vì cả hai cặp đều "loại bỏ" hoặc "giữ lại" một thứ gì đó. Khác biệt nằm ở thứ mà chúng tác động lên:
| Làm việc trên | Loại bỏ hoặc giữ lại | Ví dụ | |
|---|---|---|---|
Exclude<T, U> | một union | thành viên của union | Exclude<"a" | "b", "a"> → "b" |
Extract<T, U> | một union | thành viên của union | Extract<"a" | "b", "a"> → "a" |
Omit<T, K> | một kiểu đối tượng | thuộc tính | Omit<User, "password"> |
Pick<T, K> | một kiểu đối tượng | thuộc tính | Pick<User, "name"> |
Dùng Exclude trên kiểu đối tượng là một sơ suất phổ biến, và nó thất bại mà không báo gì:
Account không gán được cho "password", nên Exclude giữ nguyên nó. Bản thân Omit được xây dựng từ Exclude áp dụng lên các khóa: Omit<T, K> là Pick<T, Exclude<keyof T, K>>. Xem Omit để biết thêm.
Một Exclude chặt chẽ hơn
Tham số thứ hai của Exclude nhận mọi kiểu, nên một lỗi gõ sẽ không loại bỏ gì và cũng không báo lỗi. Thêm một ràng buộc sẽ buộc trình biên dịch kiểm tra rằng thứ bạn loại bỏ thật sự nằm trong union:
index.ts(7,37): error TS2344: Type '"drafft"' does not satisfy the constraint 'Status'.
Sửa lỗi chính tả là code biên dịch được, với Strict bằng "published" | "archived". Cùng mẹo này áp dụng được cho Extract và cho một Omit chặt chẽ (K extends keyof T).
Câu hỏi thường gặp
Exclude làm gì trong TypeScript?
Exclude<T, U> loại khỏi union T mọi thành viên gán được cho U. Exclude<"a" | "b" | "c", "a"> là "b" | "c". Để loại nhiều thành viên, truyền một union vào U: Exclude<T, "a" | "b">.
Exclude và Extract khác nhau thế nào?
Chúng ngược nhau. Exclude<T, U> giữ các thành viên của T không gán được cho U; Extract<T, U> chỉ giữ những thành viên gán được. Với cùng T và U, hai kết quả gộp lại chính là T.
Exclude và Omit khác nhau thế nào?
Exclude làm việc trên union type và loại bỏ thành viên của union. Omit làm việc trên kiểu đối tượng và loại bỏ thuộc tính. Exclude<User, "password"> chẳng làm được gì hữu ích (nó trả về User), còn Omit<User, "password"> cho ra kiểu đối tượng không có thuộc tính đó. Omit được xây dựng từ Exclude: nó loại khóa khỏi keyof T.
NonNullable làm gì trong TypeScript?
NonNullable<T> loại null và undefined khỏi một kiểu: NonNullable<string | null | undefined> là string. Nó được định nghĩa là T & {}, vì {} nhận mọi giá trị trừ null và undefined.
Vì sao Exclude không báo lỗi với một thành viên không tồn tại?
Tham số thứ hai của nó không có ràng buộc, nên kiểu nào cũng được chấp nhận, và một lỗi gõ như Exclude<Status, "drafft"> âm thầm không loại bỏ gì cả. Hãy định nghĩa một phiên bản chặt chẽ hơn, type StrictExclude<T, U extends T> = Exclude<T, U>, để trình biên dịch kiểm tra rằng U thuộc về T.