在昨天只有介紹了 extends
,還有其他好用的關鍵字。
typeof
關鍵字可以用來取得一個變數或表達式的型別。
const user = {
name: "John",
age: 30,
};
type UserType = typeof user; // { name: string; age: number; }
keyof
可以取得一個物件的所有 key,然後組成 Union Type。
type User = {
name: string;
age: number;
};
type UserKeys = keyof User; // 'name' | 'age'
in
是映射型別(Mapped Types)的語法,用來遍歷型別的所有屬性鍵,並根據這些鍵來創建或修改型別。
type Permissions = "read" | "write" | "delete";
type PermissionFlags = { [P in Permissions]: boolean };
// { read: boolean; write: boolean; delete: boolean }
語法基本會長這樣
T extends U ? X : Y
T
和 U
是型別,T extends U
是條件判斷。如果 T
是 U
的子型別,則結果為 X
。否則,結果為 Y
。
範例:
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
條件型別也可以疊加使用,進行複雜的邏輯處理:
type Result<T> = T extends string
? { text: T }
: T extends number
? { value: T }
: never;
infer
需要搭配 extends
和 Condition Type 使用。它可以在條件型別中推斷出某個特定的型別變數。
type ElementType<T> = T extends (infer U)[] ? U : T;
infer U
會嘗試推斷陣列中的元素型別,並將其存儲在變數 U
中。
另外 infer
也經常會搭配 never
來使用,算是一種 TypeScript 表達錯誤的方式或是表示某個型別不應該存在。
了解上面的概念後,就可以了解 Utility Types 是怎麼運作的了,都是以上概念組合而已。
Utility Types 有很多,這邊介紹比較常用到的。
Exclude
移除某個 Union Type 的值type Exclude<T, U> = T extends U ? never : T;
type AllRoles = "admin" | "editor" | "viewer";
type ForbiddenRoles = "editor" | "viewer";
type AllowedRoles = Exclude<UserRoles, ForbiddenRoles>;
// "admin"
Extract
取出某個 Union Type
的值type Extract<T, U> = T extends U ? T : never;
type AllRoles = "admin" | "editor" | "viewer";
type ActiveRoles = "admin" | "viewer";
type ActiveUserRoles = Extract<AllRoles, ActiveRoles>;
// "admin" | "viewer"
Record
可以透過給 property 和 type 的方式創造 objecttype Record<K extends keyof any, T> = {
[P in K]: T;
};
type UserPermissions = Record<"read" | "write" | "delete", boolean>;
Omit
可以移除 object 中某幾個 property 和 typetype Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P] };
Pick
可以只取 object 中某幾個 property 和 typetype Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Partial
把整個 object 的屬性改成可選的type Partial<T> = { [P in keyof T]?: T[P] | undefined };
範例:
type UserProfile = {
name: string;
age: number;
email: string;
};
type PublicUserProfile = Omit<UserProfile, "email">;
// { name: string; age: number; }
type ContactInfo = Pick<UserProfile, "email">;
// { email: string; }
type UpdateUserProfile = Partial<UserProfile>;
// { name?: string; age?: number; email?: string; }
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
範例:
function getUserInfo(userId: number): { name: string; age: number } {
return {
name: "John",
age: 30,
};
}
type UserInfo = ReturnType<typeof getUserInfo>;
// { name: string; age: number; }
type GetUserParams = Parameters<typeof getUserInfo>;
// [userId: number]
透過各種組合,也可以建立出自己的 Utility Types。
以下是一個範例,PickByValue 用來選擇型別中屬性值為指定型別的屬性:
type PickByValue<T, V> = Pick<
T,
{ [K in keyof T]: T[K] extends V ? K : never }[keyof T]
>;
type User = {
name: string;
age: number;
};
type StringProperties = PickByValue<User, string>; // { name: string; }
參考資料:
https://www.typescriptlang.org/docs/handbook/utility-types.html#intrinsic-string-manipulation-types
https://stackoverflow.com/questions/60067100/why-is-the-infer-keyword-needed-in-typescript
https://www.youtube.com/watch?v=hLZXJTm7TEk
https://chentsulin.medium.com/typescript-infer-%E7%9A%84%E5%BC%B7%E5%A4%A7%E5%8A%9F%E7%94%A8-9b43c4eac6fb