Skip to main content

[TS] Type Manipulation

keywords: generics, conditional type#

Conditional Type#

Conditional Types @ TypeScript > Handbook > Type Manipulation

// 如果 SomeType 能夠滿足 OtherType 則 SomeType 為 TrueType;否則為 FalseType
NewType = SomeType extends OtherType ? TrueType : FalseType;

基本語法#

sample code

// 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'

Conditional Type Constraints#

Example: MessageOf#

Sample code

如果想要建立一個名為 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#

sample code - 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;

Inferring within Conditional Types#

透過 infer 關鍵字來推論函式回傳的型別,並使用該型別來定義新的型別:

// https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
type GetReturnType<T> = T extends (...args: never[]) => infer R ? R : never;

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)[]

Mapped Types#

Mapped Types @ TypeScript Docs

tip

要操作物件的 type 或 interface 的屬性時,就要想到 Mapped Type。

Mapped Types 是透過 Key 取出 interface 中值的型別:

  • AllCommValues 就是透過 interface 的 key 來取得該 interface value 的型別
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;}

實際上的應用:

/**
* Mapped Type 可以透過 key 來取得 interface 中 value 的型別
*/
interface CommunicationMethods {
email: HasEmail;
phone: HasPhoneNumber;
fax: { fax: number };
}
type Method = keyof CommunicationMethods;
// keyof CommunicationMethods 會把 'email' | 'phone' | 'fax' 組成新的 type
// 所以這裡的 K 一定要滿足 'email' | 'phone' | 'fax' 的條件
// content 用的是 MappedType,所以會是 HasEmail | HasPhoneNumber | { fax: number }
function contact<K extends keyof CommunicationMethods>(
method: K,
content: CommunicationMethods[K], // 💡turning key into value -- a *mapped type*
) {
// ...
}
contact('email', { name: 'foo', email: 'foo@example.com' });
// ❌ 報錯,phone 要對應到的是 HasPhoneNumber
//contact('phone', { name: 'foo', email: 'foo@example.com' });
contact('fax', { fax: 3000 });

透過 Mapped Type 產生新的型別#

可以利用 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>;

使用 Modifiers#

在根據 key 產生新的 type 時,TypeScript 提供一些 mapping modifiers 可以讓你把原本 Type 中某屬性的特性(例如 readonly?)拿掉後,再產生新的 type。

Map 後產生新的 Key 名稱#

/**
* Mapped 後產生新的 Key 名稱
*/
type ReMapWithGet<T> = {
// 因為 P 有可能是 string | number | symbol,透過 string & P 可以限制它一定是 string
[P in keyof T as `map${Capitalize<string & P>}`]: T[P];
};
type Person = {
fullName: string;
phoneNumber: number;
};
// NewPerson 的型別會是 { mapFullName: string; mapPhoneNumber: number; }
type NewPerson = ReMapWithGet<Person>;

Map 後過濾掉特定屬性#

也可以搭配 Exclude 這個 utility 來移除原本 interface 中的屬性:

/**
* Mapped 搭配 Exclude 移除屬性
*/
type ExcludeMap<T> = {
[P in keyof T as Exclude<P, 'fullName'>]: T[P];
};
// OnlyPhoneNumber 的型別會是 type OnlyPhoneNumber = { phoneNumber: number; }
type OnlyPhoneNumber = ExcludeMap<Person>;

Template Literal Type#

// type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right"
type Corner = `${'top' | 'bottom'}-${'left' | 'right'}`;
Last updated on