[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"