[TS] Type Manipulation
keywords: generics
, conditional type
, Mapped Types
, keyof
, typeof
, infer
Conditional Type
Conditional Types @ TypeScript > Handbook > Type Manipulation
// 如果 SomeType 能夠滿足 OtherType 則 SomeType 為 TrueType;否則為 FalseType
NewType = SomeType extends OtherType ? TrueType : FalseType;
要看是否符合某個條件,可以用:
// type T = [想檢驗的條件] ? true : false
type T = 30 extends number ? true : false;
基本語法
// https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw 'unimplemented';
}
let a = createLabel('typescript'); // a is NameLabel
let b = createLabel(2.8); // b is IdLabel
let c = createLabel(Math.random() ? 'Hello' : 42); // c is NameLabel | IdLabel
let d = createLabel(['foo']); // Type 'string[]' is not assignable to type 'string'
T extends K
可以想成是 T 能夠滿足 K,也就是 T 是 K 的子集合(subset),K 的條件會比 T 來的寬鬆。
Conditional Type Constraints
Example: MessageOf
如果想要建立一個名為 MessageOf
的 Type,且其 Type 會是 T 中 message
property 的型別:
// 因為 TypeScript 無法確定 T 一定有 message 這個 property 所以會噴錯
type MessageOfWithError<T> = T['message'];
// 透過 extends 確保 T 一定會有 message 這個 property
// 如此 MessageOf 這個 type 的型別就是會 T 當中 message property 的型別
type MessageOf<T extends { message: unknown }> = T['message'];
根據定義好的 MessageOf
這個 utility type,可以產生新的 Type:
// 因為 TypeScript 無法確定 T 一定有 message 這個 property 所以會噴錯
type MessageOfWithError<T> = T['message'];
// 透過 extends 確保 T 一定會有 message 這個 property
// 如此 MessageOf 這個 type 的型別就是會 T 當中 message property 的型別
type MessageOf<T extends { message: unknown }> = T['message'];
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
// EmailMessageContents 會取得 Email 這個 Type 中 message property 的型別
type EmailMessageContents = MessageOf<Email>; // string
Example: Flatten
建立一個 utility type 可以取得另一個陣列型別內元素的型別:
// 如果 T 是陣列,則使用陣列元素的型別,否則使用 T
// 這裡 T[number] 使用的是 indexed 的方式來存取某陣列型別內元素的型別
type Flatten<T> = T extends any[] ? T[number] : T;
type Str = Flatten<string[]>; // Str 會是 string type;
type Num = Flatten<number>; // Num 會是 number type;
Example: Wrap
type Wrap<T> = T extends { length: number } ? [T] : T;
type stringArr = Wrap<string>;
extends 蠻特別的地方
type S = 'foo' | 'bar' | 35;
// 如果是直接使用 Extends 的話,會是直接比較 "'foo' | 'bar' | 35 extends string"
type T = S extends string ? true : false; // false
// 但如果是用泛型當成參數帶入的話,則是會一一比較
type T<C> = C extends string ? true : any;
type K = T<S>; // any。實際上是 true | true | any => 最後得到 any
Inferring within Conditional Types
infer
這個關鍵字只能用在 conditional type declaration 中。
透過 infer
關鍵字來推論函式回傳的型別,並使用該型別來定義新的型別:
// https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
type GetReturnType<T> = T extends (...args: never[]) => infer R ? R : never;
把 onXXX
變成 handleXXX
的型別:
type Handler<S extends string> = S extends `on${infer P}` ? `handle${P}` : S;
type Foo = Handler<'onClick'>; // type Foo = "handleClick"
如果搭配 Mapped Type 還可以更多樣:
interface IEventHandlers {
onClick: () => void;
onChange: () => void;
}
// 把 onXXX 變成 handleXXX
type Handler<S extends string> = S extends `on${infer P}` ? `handle${P}` : S;
// 把物件的 key 都做轉換
type handleEvents<T> = {
[K in keyof T as `${Handler<string & K>}`]: T[keyof T];
};
type HandleEvents = handleEvents<IEventHandlers>;
// type HandleEvents = {
// handleClick: (() => void) | (() => void);
// handleChange: (() => void) | (() => void);
// }
Distributive Conditional Types
// ToArray 是一個 utility type,會產生帶有特定型別元素的陣列
type ToArray<T> = T extends any ? T[] : never;
// 預設會使用分配律(distributive law)
// 所以型別會變成 ToArray<string> | ToArray<number>
// 因此等同於 string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;
// 如果想要避免分配律發生,可以在 extends 前後的型別都加上 []
type ToArrayNotDist<T> = [T] extends [any] ? T[] : never;
type StrOrNumArr = ToArrayNotDist<string | number>; // (string | number)[]
keyof
Keyof Type Operator @ TypeScript > Handbook > Type Manipulation
keyof
可以很方便的把物件類的型別的 key 給取出:
enum MarriedStatus {
MARRIED = 'married',
SINGLE = 'single',
}
type Person = {
firstName: string;
lastName: string;
age: number;
marriedStatus: MarriedStatus;
};
type P = keyof Person; // "firstName" | "lastName" | "age" | "marriedStatus"
keyof 搭配 & string
另外,keyof
很適合搭配 & string
來使用,如此可以確定取出來的 type 一定是 string type 而不會是 symbol
或其他可能,而且 VSCode 在滑鼠以上去時,也會直接顯示對應的 type,例如,"firstName" | "lastName" | "age" | "marriedStatus"
,而不是單純寫 keyof Person
:
type P = keyof Person & string; // "firstName" | "lastName" | "age" | "marriedStatus"
typeof
typeof Type Operator @ TypeScript > Handbook > Type Manipulation
透過 typeof
可以直接取得 TS 針對 value 所推論出的型別,並拿來產生新的 type 使用:
const me = {
firstName: 'Aaron',
lastName: 'Chen',
email: 'pjchender@gmail.com',
};
type Me = typeof me;
// type Me = {
// firstName: string;
// lastName: string;
// email: string;
// }
typeof 取出 array 中的元素變成 Type(array to type)
const colors = ['red', 'green', 'blue'] as const;
type Color = Uppercase<typeof colors[number]>; // "RED" | "GREEN" | "BLUE"
type ActionType = `ADJUST_${Color}`; // "ADJUST_RED" | "ADJUST_GREEN" | "ADJUST_BLUE"
Indexed Access Types
Indexed Access Types @ TypeScript > Handbook > Type Manipulation
我們可以直接透過 index 的方式取得物件類型別的 value 的型別:
type Device = {
name: string;
model: string;
price: number;
isLaunch: boolean;
};
// indexed access types
type DeviceName = Device['name']; // type DeviceName = string
// 在裡面可以使用聯集(union)
type T = Device['name' | 'isLaunch']; // type T = string | boolean
搭配 keyof
如果搭配 keyof
的話,可以直接取出該物件所有 value 的型別:
type Device = {
name: string;
model: string;
price: number;
isLaunch: boolean;
};
type ValueOfDevice = Device[keyof Device]; // type ValueOfDevice = string | number | boolean
另一個使用的例子:
interface CommunicationMethods {
email: HasEmail;
phone: HasPhoneNumber;
fax: { fax: number };
}
type AllCommKeys = keyof CommunicationMethods; // "email" | "phone" | "fax"
type AllCommValues = CommunicationMethods[keyof CommunicationMethods]; // HasEmail | HasPhoneNumber | {fax: number;}
搭配 typeof 與 Array
如果是陣列的話,則可以搭配 typeof
取出陣列所有可能的值:
const arr = ['number', true, 3];
type A = typeof arr[number]; // type A = string | number | boolean
如果該陣列是 Readonly Array 的話 ,則可以很方便的組出 union string:
// brands 是 Readonly Array
const brands = ['apple', 'samsung', 'xiaomi', 'google'] as const;
type B = typeof brands[number]; // type B = "apple" | "samsung" | "xiaomi" | "google"
如果是 array of object 的話,則可以方便的取出 Object 的型別:
const brands = [
{ name: 'apple', model: 'iphone' },
{ name: 'samsung', model: 'galaxy' },
{ name: 'google', model: 'pixel' },
];
type B = typeof brands[number];
// type B = {
// name: string;
// model: string;
// }
Index Signatures, Mapped Types & In Operator
- Mapped Types @ TypeScript > Handbook > Type Manipulation
- 認識 TypeScript 中 的型別魔術師:Mapped Type
- Day14:什麼!TypeScript 中還有迴圈的概念 - 用 Mapped Type 操作物件型別
Index Signatures
在 TypeScript 中可以使用 index signatures 來建立類似 dictionary 的型別:
type Device = {
name: string;
model: string;
price: number;
isLaunch: boolean;
};
// index signature
type DeviceDict = {
[key: string]: Device | undefined;
};
// Dict:寫成泛型
type Dict<T> = {
[key: string]: T | undefined;
};
如此可以讓某個物件的 key 一定是 string
,value 一定是 Device | undefined
。
但如果我們不想要讓 key 的範圍這麼廣泛,除了可以一樣用 index signature 一一列舉之外,並不能透過 union 的方式來寫:
// ⭕️ 這樣列舉是可以的
type DeviceDict = {
[key: string]: Device | undefined;
// 列舉可被接受的 key
apple: Device;
samsung: Device;
};
// ❌ 不能這樣寫,會出現錯誤
// An index signature parameter type cannot be a literal type or generic type.
// Consider using a mapped object type instead
type DeviceDict = {
[key: 'apple' | 'samsung']: Device | undefined;
};
而 Mapped Types 則提供更方便的方式來定義和操作型別。
Index signature 和 Mapped Type 很大的差別在於,index signature 的 key 適用的是 string
或 number
;Mapped Type 的 key 則是限縮後的 string (subset of string)或 number。
Mapped Types & in operator
透過 Mapped Type 將可以更容易的操作物件類的型別,也可以使用 Mapped Typed 來產生新的型別。
操作物件型別時,要很快聯想到 Mapped Types,不論是想改變物件屬性 key 的名稱或 value 的型別,都可以透過。Mapped Types 達到。
in:loop over 的概念
Mapped Type 搭配 in
時,要把它想成類似 JavaScript 中的 for...in
或 for...of
使用,它會列舉 in 後面的所有值。
當我們寫 [P in K]
,就類似 for (item of arr)
的感覺,P
對應的是 item,K
對應到的則是 arr
P
表示的是每次疊代時的元素值,也就是會多一個 P (Parameter) 可以使用,P 可以換成是任何名字。這裡依序會是 apple, samsung, ...。
type T = {
[P in 'apple' | 'samsung' | 'xiaomi' | 'google']: any;
};
所以當使用 [key in 'apple' | 'samsung' | 'xiaomi' | 'google']
,會產生出來的型別是 apple, Samsung 等都需要出現在物件中,而每次 loop 的 element 則會保存在 key
這個 type 中:
type DeviceRecord = {
// 搭配 in operator,key 會是每次 loop 的 element
[key in 'apple' | 'samsung' | 'xiaomi' | 'google']: Device;
// 如果不同的 key 的 value 對應到不同的型別,可以寫像這樣
// K 會是每次 loop 的 element
// [K in 'apple' | 'samsung' | 'xiaomi' | 'google']: K;
};
// 會變成
type DeviceRecord = {
apple: Device;
samsung: Device;
xiaomi: Device;
google: Device;
};
P in T
的使用可以想成是 JS 中 for...of
或 for...in
的方法,它會疊代(iterate)in
後面的每個元素,而 P
是多產生的一個變數,表示的是每次迴圈跑到的元素。
Mapped Type 可以被視為 index signatures 的子集合(subset)。搭配 in
operator 時,可以在 key 的地方使用 union type:
type Device = {
name: string;
model: string;
price: number;
isLaunch: boolean;
};
// Mapped Type
type DeviceRecord = {
// 搭配 in operator
[key in 'apple' | 'samsung' | 'xiaomi' | 'google']: Device;
};
in
後面可以放的是 string
、number
或 symbol
型別,也就會是 keyof any
。
in:取出 enum 的 value 產生新的 Object Type
enum GENDER {
MALE = 'male',
FEMALE = 'female',
}
type Gender = { [P in GENDER]: string };
// type Gender = {
// male: string;
// female: string;
// }
in keyof:取出 object 的 key 產生新的 Object Type
in keyof
的概念是同時使用了 indexed access type 中「用 key 來取得 value 的型別」,以及 Mapped Type 中 in
的概念而成:
interface IGender {
Male: string;
Female: string;
}
type Gender = { [P in keyof IGender]: string };
// type Gender = {
// Male: string;
// Female: string;
// };
remapping
可以利用 Mapped Types 的特性來建立新的 type:
/**
* 利用 Mapped Type 產生新的型別
*/
type OptionsFlags<T> = {
[P in keyof T]: boolean;
};
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
// P in keyof T 指的會是 "darkMode" | "newUserProfile"
// 因此 FeatureOptions 的型別會是 { darkMode: boolean; newUserProfile: boolean }
type FeatureOptions = OptionsFlags<FeatureFlags>;
利用這樣的特性也可以原封不動的產生規則一樣的新的 Type:
type Clone<T> = {
[P in keyof T]: T[P];
};
type Person = {
fullName: string;
phoneNumber: number;
};
type NewPerson = Clone<Person>;
進一步搭配 as 使用(強大)
- 要留意
as
後的 template string 要使用string & K
來縮限K
的型別
interface IEvent {
click: () => void;
change: () => void;
}
// 要留意 as 後的 template string 要使用 string & K 來縮限 K 的型別
type EventHandler<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: T[K];
};
type EventHandlers = EventHandler<IEvent>;
type EventHandlers = {
onClick: () => void;
onChange: () => void;
};