Câu lệnh kiểu điều kiện `extends` trong TypeScript chỉ hoạt động nếu được biểu diễn bằng Generics?
Tôi đang cố gắng hiểu rõ hơn về từ khóa extends trong TypeScript và các ứng dụng tiềm năng của nó.
Một điều mà tôi đã gặp là hai tiện ích tích hợp sẵn, Extract và Exclude, tận dụng cả extends và Kiểu Điều kiện.
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
Tôi đã thử nghịch để hiểu rõ hơn cách mà 'thu hẹp', hay nói chính xác hơn là 'lọc tập con' hoạt động, và đã thử tạo triển khai của riêng mình chỉ để xem nó hoạt động như thế nào, và đã gặp phải hành vi thực sự kỳ lạ này:
Liên kết đến Ví dụ Playground:
type ValueSet = string | 'lol' | 0 | {a: 1} | number[] | 643;
type CustomExclude<T, U> = T extends U ? T : never;
// this works:
// type Result1 = 0 | 643
type Result1 = CustomExclude<ValueSet, number>;
// but this doesn't?
// type Result2 = never
type Result2 = ValueSet extends number ? ValueSet : never;
Tại sao điều đó lại xảy ra?
Tôi mong cả hai trường hợp sẽ trả về tập hợp con đúng của kiểu, nhưng kiểu có điều kiện chỉ hoạt động nếu được biểu thị thông qua Generics.
Ai đó có thể giải thích cho tôi logic đằng sau điều này không?
Câu trả lời #1
Vui lòng xem các loại điều kiện phân phối:
Khi các kiểu điều kiện hoạt động trên một kiểu tổng quát, chúng trở nên phân phối khi được cung cấp một kiểu hợp. Ví dụ, hãy xem xét các ví dụ sau:
type ToArray<Type> = Type extends any ? Type[] : never;
Nếu chúng ta chèn một loại hợp nhất vào ToArray, thì kiểu điều kiện sẽ được áp dụng cho từng thành viên của hợp nhất đó.
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]
Do đó, nếu bạn sử dụng extends với kiểu generic, toàn bộ kiểu điều kiện sẽ áp dụng cho từng phần tử trong hợp nhất.
Nếu bạn sử dụng extends với kiểu không tổng quát, như bạn sử dụng trong ví dụ thứ hai của mình, các kiểu điều kiện sẽ áp dụng cho toàn bộ kiểu đó.
Bạn thậm chí có thể tắt phân phối trong ví dụ đầu tiên của mình. Chỉ cần bao các generic của bạn trong dấu ngoặc vuông:
type ValueSet = string | 'lol' | 0 | {a: 1} | number[] | 643;
type CustomExclude<T, U> = [T] extends [U] ? T : never;
// never
type Result1 = CustomExclude<ValueSet, number>;
Kiểu tổng quát được bao trong dấu ngoặc vuông được coi như một kiểu không tổng quát, giống như trong ví dụ đầu tiên của bạn.
Trong thực tế, mẫu này rất hữu ích. Thường thì người ta sử dụng T extends any chỉ để bật tính phân phối.
Giả sử bạn có một kiểu đối tượng nào đó. Bạn muốn lấy tất cả các khóa và áp dụng một số biến đổi cho chúng. Nói cách khác là ánh xạ chúng. Hãy xem xét ví dụ sau:
type Foo = {
name: string;
age: number
}
// non verbose approach, distributivity
type ChangeKey<T> = keyof T extends string ? `${keyof T}-updated` : never
type Result = ChangeKey<Foo>
// middle verbose approach
type ChangeKey1<T> = {
[Prop in keyof T]: Prop extends string ? `${Prop}-updated` : never
}[keyof T]
type Result1 = ChangeKey1<Foo>
// verbose approach
type ChangeKey2<T extends Record<string, unknown>> = keyof {
[Prop in keyof T as Prop extends string ? `${Prop}-updated` : never]: never
}
type Result2 = ChangeKey2<Foo>
Như bạn có thể đã nhận thấy, ChangeKey tinh tế hơn những cái khác.
Câu trả lời #2
Đoạn mã thứ hai đang thực hiện một kiểm tra duy nhất để xem liệu toàn bộ kiểu có mở rộng từ number hay không. Nếu có, nó sẽ trả về toàn bộ kiểu, nếu không nó sẽ trả về never. Phiên bản với generics sẽ lần lượt kiểm tra tất cả các kiểu riêng lẻ trong hợp nhất (trước string, sau đó "lol", rồi 0 v.v.) và đánh giá từng một cách riêng lẻ. Sau đó bạn sẽ nhận được một hợp nhất của những kiểu riêng lẻ nào còn tồn tại.
Có thể lấy một giá trị không phải never từ ví dụ thứ hai của bạn, nhưng chỉ khi mọi giá trị có thể đều là số. Ví dụ:
type Example = 1 | 3 | 5;
type Example2 = Example extends number ? Example : never;
// Example2 is 1 | 3 | 5
