我們深入了解 TypeScript 官方提供的全局工具類型。
在 TypeScript 中提供了許多自帶的工具類型徘郭,因為這些類型都是全局可用的靠益,所以無須導入即可直接使用。了解了基礎的工具類型后崎岂,我們不僅知道 TypeScript 如何利用前幾講介紹的基礎類型知識實現(xiàn)這些工具類型捆毫,還知道如何更好地利用這些基礎類型,以免重復造輪子冲甘,并能通過這些工具類型實現(xiàn)更復雜的類型绩卤。
根據(jù)使用范圍途样,我們可以將工具類型劃分為操作接口類型、聯(lián)合類型濒憋、函數(shù)類型何暇、字符串類型這 4 個方向,下面一一介紹凛驮。
操作接口類型
Partial
Partial 工具類型可以將一個類型的所有屬性變?yōu)榭蛇x的裆站,且該工具類型返回的類型是給定類型的所有子集,下面我們看一個具體的示例:
type Partial<T> = {
? [P in keyof T]?: T[P];
};
interface Person {
? name: string;
? age?: number;
? weight?: number;
}
type PartialPerson = Partial<Person>;
// 相當于
interface PartialPerson {
? name?: string;
? age?: number;
? weight?: number;
}
在上述示例中黔夭,我們使用映射類型取出了傳入類型的所有鍵值宏胯,并將其值設定為可選的。
Required
與 Partial 工具類型相反本姥,Required 工具類型可以將給定類型的所有屬性變?yōu)楸靥畹募缗郏旅嫖覀兛匆粋€具體示例。
type Required<T> = {
? [P in keyof T]-?: T[P];
};
type RequiredPerson = Required<Person>;
// 相當于
interface RequiredPerson {
? name: string;
? age: number;
? weight: number;
}
在上述示例中婚惫,映射類型在鍵值的后面使用了一個 - 符號氛赐,- 與 ? 組合起來表示去除類型的可選屬性,因此給定類型的所有屬性都變?yōu)榱吮靥睢?/p>
Readonly
Readonly 工具類型可以將給定類型的所有屬性設為只讀先舷,這意味著給定類型的屬性不可以被重新賦值艰管,下面我們看一個具體的示例。
type Readonly<T> = {
? readonly [P in keyof T]: T[P];
};
type ReadonlyPerson = Readonly<Person>;
// 相當于
interface ReadonlyPerson {
? readonly name: string;
? readonly age?: number;
? readonly weight?: number;
}
在上述示例中蒋川,經(jīng)過 Readonly 處理后牲芋,ReadonlyPerson 的 name、age尔破、weight 等屬性都變成了 readonly 只讀街图。
Pick
Pick 工具類型可以從給定的類型中選取出指定的鍵值,然后組成一個新的類型懒构,下面我們看一個具體的示例餐济。
type Pick<T, K extends keyof T> = {
? [P in K]: T[P];
};
type NewPerson = Pick<Person, 'name' | 'age'>;
// 相當于
interface NewPerson {
? name: string;
? age?: number;
}
在上述示例中,Pick工具類型接收了兩個泛型參數(shù)胆剧,第一個 T 為給定的參數(shù)類型絮姆,而第二個參數(shù)為需要提取的鍵值 key。有了參數(shù)類型和需要提取的鍵值 key秩霍,我們就可以通過映射類型很容易地實現(xiàn) Pick 工具類型的功能篙悯。
Omit
與 Pick 類型相反,Omit 工具類型的功能是返回去除指定的鍵值之后返回的新類型铃绒,下面我們看一個具體的示例:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type NewPerson = Omit<Person, 'weight'>;
// 相當于
interface NewPerson {
? name: string;
? age?: number;
}
在上述示例中鸽照,Omit 類型的實現(xiàn)使用了前面介紹的 Pick 類型。我們知道 Pick 類型的作用是選取給定類型的指定屬性颠悬,那么這里的 Omit 的作用應該是選取除了指定屬性之外的屬性矮燎,而 Exclude 工具類型的作用就是從入?yún)?T 屬性的聯(lián)合類型中排除入?yún)?K 指定的若干屬性定血。
Tips:操作接口類型這一小節(jié)所介紹的工具類型都使用了映射類型。通過映射類型诞外,我們可以對原類型的屬性進行重新映射澜沟,從而組成想要的類型。
聯(lián)合類型
Exclude
在介紹 Omit 類型的實現(xiàn)中峡谊,我們使用了 Exclude 類型茫虽。通過使用 Exclude 類型,我們從接口的所有屬性中去除了指定屬性既们,因此濒析,Exclude 的作用就是從聯(lián)合類型中去除指定的類型。
下面我們看一個具體的示例:
type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<'a' | 'b' | 'c', 'a'>; // => 'b' | 'c'
type NewPerson = Omit<Person, 'weight'>;
// 相當于
type NewPerson = Pick<Person, Exclude<keyof Person, 'weight'>>;
// 其中
type ExcludeKeys = Exclude<keyof Person, 'weight'>; // => 'name' | 'age'
在上述示例中啥纸,Exclude 的實現(xiàn)使用了條件類型悼枢。如果類型 T 可被分配給類型 U ,則不返回類型 T脾拆,否則返回此類型 T ,這樣我們就從聯(lián)合類型中去除了指定的類型莹妒。
再回看之前的 NewPerson 類型的例子名船,我們也就很明白了。在 ExcludeKeys 中旨怠,如果 Person 類型的屬性是我們要去除的屬性渠驼,則不返回該屬性,否則返回其類型鉴腻。
Extract
Extract 類型的作用與 Exclude 正好相反迷扇,Extract 主要用來從聯(lián)合類型中提取指定的類型,類似于操作接口類型中的 Pick 類型爽哎。
下面我們看一個具體的示例:
type Extract<T, U> = T extends U ? T : never;
type T = Extract<'a' | 'b' | 'c', 'a'>; // => 'a'
通過上述示例蜓席,我們發(fā)現(xiàn) Extract 類型相當于取出兩個聯(lián)合類型的交集。
此外课锌,我們還可以基于 Extract 實現(xiàn)一個獲取接口類型交集的工具類型厨内,如下示例:
type Intersect<T, U> = {
? [K in Extract<keyof T, keyof U>]: T[K];
};
interface Person {
? name: string;
? age?: number;
? weight?: number;
}
interface NewPerson {
? name: string;
? age?: number;
}
type T = Intersect<Person, NewPerson>;
// 相當于
type T = {
? name: string;
? age?: number;
};
在上述的例子中,我們使用了 Extract 類型來提取兩個接口類型屬性的交集渺贤,并使用映射類型生成了一個新的類型雏胃。
NonNullable
NonNullable 的作用是從聯(lián)合類型中去除 null 或者 undefined 的類型。如果你對條件類型已經(jīng)很熟悉了志鞍,那么應該知道如何實現(xiàn) NonNullable 類型了瞭亮。
下面看一個具體的示例:
type NonNullable<T> = T extends null | undefined ? never : T;
// 等同于使用 Exclude
type NonNullable<T> = Exclude<T, null | undefined>;
type T = NonNullable<string | number | undefined | null>; // => string | number
在上述示例中,如果 NonNullable 傳入的類型可以被分配給 null 或是 undefined 固棚,則不返回該類型统翩,否則返回其具體類型仙蚜。
Record
Record 的作用是生成接口類型,然后我們使用傳入的泛型參數(shù)分別作為接口類型的屬性和值唆缴。
下面我們看一個具體的示例:
type Record<K extends keyof any, T> = {
? [P in K]: T;
};
type MenuKey = 'home' | 'about' | 'more';
interface Menu {
? label: string;
? hidden?: boolean;
}
const menus: Record<MenuKey, Menu> = {
? about: { label: '關(guān)于' },
? home: { label: '主頁' },
? more: { label: '更多', hidden: true },
};
在上述示例中鳍征,Record 類型接收了兩個泛型參數(shù):第一個參數(shù)作為接口類型的屬性,第二個參數(shù)作為接口類型的屬性值面徽。
需要注意:這里的實現(xiàn)限定了第一個泛型參數(shù)繼承自keyof any艳丛。
在 TypeScript 中,keyof any 指代可以作為對象鍵的屬性趟紊,如下示例:
type T = keyof any; // => string | number | symbol
說明:目前氮双,JavaScript 僅支持string、number霎匈、symbol作為對象的鍵值戴差。
函數(shù)類型
ConstructorParameters
ConstructorParameters 可以用來獲取構(gòu)造函數(shù)的構(gòu)造參數(shù),而 ConstructorParameters 類型的實現(xiàn)則需要使用 infer 關(guān)鍵字推斷構(gòu)造參數(shù)的類型铛嘱。
關(guān)于 infer 關(guān)鍵字暖释,我們可以把它當成簡單的模式匹配來看待。如果真實的參數(shù)類型和 infer 匹配的一致墨吓,那么就返回匹配到的這個類型球匕。
下面看一個具體的示例:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (
? ...args: infer P
) => any
? ? P
? : never;
class Person {
? constructor(name: string, age?: number) {}
}
type T = ConstructorParameters<typeof Person>; // [name: string, age?: number]
在上述示例中,ConstructorParameters 泛型接收了一個參數(shù)帖烘,并且限制了這個參數(shù)需要實現(xiàn)構(gòu)造函數(shù)亮曹。于是,我們通過 infer 關(guān)鍵字匹配了構(gòu)造函數(shù)內(nèi)的構(gòu)造參數(shù)秘症,并返回了這些參數(shù)照卦。因此,可以看到第 7 行匹配了 Person 構(gòu)造函數(shù)的兩個參數(shù)乡摹,并返回了一個元組類型 [string, number] 給類型別名 T役耕。
Parameters
Parameters 的作用與 ConstructorParameters 類似,Parameters 可以用來獲取函數(shù)的參數(shù)并返回序?qū)Υ狭缦率纠?/p>
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type T0 = Parameters<() => void>; // []
type T1 = Parameters<(x: number, y?: string) => void>; // [x: number, y?: string]
在上述示例中蹄葱,Parameters 的泛型參數(shù)限制了傳入的類型需要滿足函數(shù)類型。
ReturnType
ReturnType 的作用是用來獲取函數(shù)的返回類型锄列,下面我們看一個具體的示例:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type T0 = ReturnType<() => void>; // => void
type T1 = ReturnType<() => string>; // => string
在上述示例中图云,ReturnType的泛型參數(shù)限制了傳入的類型需要滿足函數(shù)類型。
ThisParameterType
ThisParameterType 可以用來獲取函數(shù)的 this 參數(shù)類型邻邮,下面看一個具體的示例:
type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;
type T = ThisParameterType<(this: Number, x: number) => void>; // Number
在上述示例的第 1 行中竣况,因為函數(shù)類型的第一個參數(shù)聲明的是 this 參數(shù)類型,所以我們可以直接使用 infer 關(guān)鍵字進行匹配并獲取 this 參數(shù)類型筒严。在示例的第 2 行丹泉,類型別名 T 得到的類型就是 Number情萤。
ThisType
ThisType 的作用是可以在對象字面量中指定 this 的類型。ThisType 不返回轉(zhuǎn)換后的類型摹恨,而是通過 ThisType 的泛型參數(shù)指定 this 的類型筋岛,下面看一個具體的示例:
注意:如果你想使用這個工具類型,那么需要開啟noImplicitThis的 TypeScript 配置晒哄。
type ObjectDescriptor<D, M> = {
? data?: D;
? methods?: M & ThisType<D & M>; // methods 中 this 的類型是 D & M
};
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
? let data: object = desc.data || {};
? let methods: object = desc.methods || {};
? return { ...data, ...methods } as D & M;
}
const obj = makeObject({
? data: { x: 0, y: 0 },
? methods: {
? ? moveBy(dx: number, dy: number) {
? ? ? this.x += dx; // this => D & M
? ? ? this.y += dy; // this => D & M
? ? },
? },
});
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
在上述示例子中睁宰,methods 屬性的 this 類型為 D & M,在上下文中指代 { x: number, y: number } & { moveBy(dx: number, dy: number): void }寝凌。
ThisType 工具類型只是提供了一個空的泛型接口柒傻,僅可以在對象字面量上下文中被 TypeScript 識別,如下所示:
interface ThisType<T> {}
也就是說該類型的作用相當于任意空接口较木。
OmitThisParameter
OmitThisParameter 工具類型主要用來去除函數(shù)類型中的 this 類型红符。如果傳入的函數(shù)類型沒有顯式聲明 this 類型,那么返回的仍是原來的函數(shù)類型伐债。
下面看一個具體的示例:
type OmitThisParameter<T> = unknown extends ThisParameterType<T>
? ? T
? : T extends (...args: infer A) => infer R
? ? (...args: A) => R
? : T;
type T = OmitThisParameter<(this: Number, x: number) => string>; // (x: number) => string
在上述示例中预侯, ThisParameterType 類型的實現(xiàn)如果傳入的泛型參數(shù)無法推斷 this 的類型,則會返回 unknown 類型峰锁。在OmitThisParameter 的實現(xiàn)中雌桑,第一個條件語句如果傳入的函數(shù)參數(shù)沒有 this 類型,則返回原類型祖今;否則通過 infer 分別獲取函數(shù)參數(shù)和返回值的類型構(gòu)造一個新的沒有 this 的函數(shù)類型,并返回這個函數(shù)類型拣技。
字符串類型
模板字符串
TypeScript 自 4.1版本起開始支持模板字符串字面量類型千诬。為此,TypeScript 也提供了 Uppercase膏斤、Lowercase徐绑、Capitalize、Uncapitalize這 4 種內(nèi)置的操作字符串的類型莫辨,如下示例:
// 轉(zhuǎn)換字符串字面量到大寫字母
type Uppercase<S extends string> = intrinsic;
// 轉(zhuǎn)換字符串字面量到小寫字母
type Lowercase<S extends string> = intrinsic;
// 轉(zhuǎn)換字符串字面量的第一個字母為大寫字母
type Capitalize<S extends string> = intrinsic;
// 轉(zhuǎn)換字符串字面量的第一個字母為小寫字母
type Uncapitalize<S extends string> = intrinsic;
type T0 = Uppercase<'Hello'>; // => 'HELLO'
type T1 = Lowercase<T0>; // => 'hello'
type T2 = Capitalize<T1>; // => 'Hello'
type T3 = Uncapitalize<T2>; // => 'hello'
在上述示例中傲茄,這 4 種操作字符串字面量工具類型的實現(xiàn)都是使用 JavaScript 運行時的字符串操作函數(shù)計算出來的,且不支持語言區(qū)域設置沮榜。以下代碼是這 4 種字符串工具類型的實際實現(xiàn)盘榨。
function applyStringMapping(symbol: Symbol, str: string) {
? switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
? ? case IntrinsicTypeKind.Uppercase:
? ? ? return str.toUpperCase();
? ? case IntrinsicTypeKind.Lowercase:
? ? ? return str.toLowerCase();
? ? case IntrinsicTypeKind.Capitalize:
? ? ? return str.charAt(0).toUpperCase() + str.slice(1);
? ? case IntrinsicTypeKind.Uncapitalize:
? ? ? return str.charAt(0).toLowerCase() + str.slice(1);
? }
? return str;
}
在上述代碼中可以看到,字符串的轉(zhuǎn)換使用了 JavaScript 中字符串的 toUpperCase 和 toLowerCase 方法蟆融,而不是 toLocaleUpperCase 和 toLocaleLowerCase草巡。其中 toUpperCase 和 toLowerCase 采用的是 Unicode 編碼默認的大小寫轉(zhuǎn)換規(guī)則。