Kiểu điều kiện, vận dụng infer với kiểu điều kiện
Việc vận dụng điều kiện để xác định kiểu trong typescript là phương cách rất linh hoạt. Trong thực tế, có rất nhiều kiểu utility có sẵn hay kiểu generic cần sử dụng điều kiện và infer
Trong Typescript, chúng ta cũng có thể xác định được kiểu của giá trị input và output thông qua điều kiện, tương tự như cú pháp điều kiện rút gọn của JS: T extends U ? X : Y
// Ví dụ như chúng ta có
interface Admin { role: string;}
interface User { name: string }
interface SchoolAdmin extends Admin {
name: string;
}
type PermissionType = SchoolAdmin extends Admin ? "admin" : "normal"
Tuy nhiên cách dùng này chưa thực sự hữu dụng, và cũng chưa đúng với mục đích thực sự của kiểu điều kiện.
// Chúng ta đi tìm một ví dụ khác hữu dụng hơn bằng việc phối hợp với generic
// Ta có một kiểu với union type
type T1 = "a" | "b" | undefined | null | () => void
// Tuy nhiên ta lại muốn tách undefined ra khỏi điều kiện này
type RemoveUndefined<UType> = UType extends undefined ? never : UType;
type T2 = RemoveUndefined<T1>
// T2 = "a" | "b" | null
Chúng ta sẽ nâng cấp tính “generic” của nó lên một chút nữa
type RemoveType<U, TRemove> = U extends TRemove ? never : T
type T3 = RemoveType<T1, null>
// "a" | "b" | undefined
// Exclude
type T3 = RemoveType<T1, null>;
// Trong Typescript họ đã có sẵn một kiểu tiện ích tương tự
type T4 = Exclude<Union, ExcludeMembers>
Bây giờ các bạn thử triển khai kiểu tiện ích Extract<Type, Union> , NonNullable<T>
// Extract<Type, Union>
type T1 = "a" | "b" | undefined | null | (() => void)
type ExtractType<T, U> = T extends U ? T : never
type T5 = ExtractType<T1, Function>
// NonNullable<T>
type NonNullableType<T> = T extends null | undefined ? never : T;
type T6 = NonNullableType<T1>;
Ứng dụng thực tế nâng cao
interface User {
id: number;
name: string;
age: number;
updateName(newName: string): void;
}
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
type T5 = FunctionPropertyNames<User>; // "updateName"
type T6 = FunctionProperties<User>; // { updateName: (newName: string) => void; }
type T7 = NonFunctionPropertyNames<User>; // "id" | "name" | "age"
type T8 = NonFunctionProperties<User>; // { id: number; name: string; age: number; }
Infer
/* Sử dụng infer
Từ khóa infer thường được sử dụng trong điều kiện để gán kiểu
*/
// Giờ mình cho một ví dụ
type ArrayElementType<T> = T extends (infer E)[] ? E : T;
// Ở đây infer sẽ khai báo một type mới là E
type T8 = ArrayElementType<number[]>;
// Điều kiện này thỏa mãn do kiểu infer là number array => T8 có kiểu number
type T9 = ArrayElementType<{ age: number }>;
// điều kiện này không thỏa mãn nên lấy trực tiếp type bên trong
type T10 = ArrayElementType<[string, number]>
// Điều kiện này cũng thõa mãn nên nó lấy kiểu union là string | number
Từ khóa infer cho phép chúng ta xác định kiểu một cách cụ thể hơn, kể cả trong trường hợp đó là một element thuộc một đối tượng
type ElementIdType<T> = T extends {id: infer IdType} ? IdType : T
type UserIdType = ElementIdType<{id: number, name: string}>
type ProjectIdType = ElementIdType<{id: string, title: string}>
Đây rõ ràng là một cách rất hay để xác định kiểu
Kiểu điều kiện phối hợp với infer là một bộ đôi rất hay, trong các kiểu tiện ích của Typescript, ReturnType<T> , InstanceType<T> cũng triển khai bằng cách phối hợp 2 cái này.
Triển khai ReturnType đây là kiểu xác định một kiểu dựa trên kiểu giá trị trả về của một hàm nào đó.
type FunctionReturnType<T> = T extends (...args: any) => infer R ? R : T;
type R = FunctionReturnType<(name: string) => string>;
type E = FunctionReturnType<{ name: string }>;
// Do <T> đang là generic, vì vậy chúng ta có thể gán được kiểu bất kỳ
Để có thể hoàn thiện tính năng của FunctionReturnType tương tự như ReturnType , ta chỉ việc xác định T được extends từ function
type FunctionReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : T;
Và giờ ta có FunctionReturnType hoạt động giống hệt ReturnType
Ta thử triển khai InstanceType , đây là kiểu xác định một kiểu dựa trên kiểu của instance của một class nào đó
class User {
age = 20;
name = "John";
}
const userA = new User();
// ta có kiểu cùa userA là User
// đây là kiểu được TS ngầm infer, khá thông minh
// để xác định một user B object có cùng kiểu với userA
// ta có thể sử dụng cách này
const userB: typeof userA = {
age: 25,
name: "hello"
};
// tuy nhiên dùng như vậy rất bất tiện và không thực tế
// nên chúng ta sẽ xác định một kiểu generic dựa trên instance của class User
type ClassInstance<T extends new (...args: any) => any> = T extends new (
...args: any
) => infer R
? R
: any;
type T10 = ClassInstance<typeof User>;
Như vậy, chúng ta đã nắm được cách tạo kiểu generic dựa trên điều kiện và vận dụng infer. Trên thực tế, việc vận dụng có thể phức tạp hơn nhưng chúng ta sẽ tạm dừng ở đây thôi