TypeScript高級類型-實用技巧
預備知識
類型遞歸
在 TypeScript 中有這樣一個內置類型工具 Required<T>
,它可以將對象類型 T
上的所有 可選屬性
轉化爲 必填屬性
。
先看一下 Required<T>
是如何定義的
/**
* Make all properties in T required
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
類型轉化過程如下:
interface IUser {
name: string;
age: number;
sex?: string;
department?: string;
address?: string;
}
type requiredUser = Required<IUser>
/* type requiredUser = {
name: string;
age: number;
sex: string;
department: string;
address: string;
} */
經過 Required<T>
處理後,可以看到類型 requiredUser
裏面的所有屬性都被轉爲了必選屬性。
現在假如我們有如下一個具有嵌套的類型,再看一下轉化結果
interface ICompany {
id: string;
companyName: string;
companyAddress?: string;
}
interface IUser {
name: string;
age: number;
sex?: string;
department?: string;
address?: string;
company: ICompany; // 嵌套類型中包含可選屬性
}
type requiredUser = Required<IUser>
/* type requiredUser = {
name: string;
age: number;
sex: string;
department: string;
address: string;
company: ICompany; // 嵌套類型中的可選類型未被轉爲必選屬性
} */
轉化結果發現嵌套類型 ICompany
中的可選屬性並未被轉化爲必選屬性,這是因爲 Required<T>
只被用於當前類型 T
的轉化,而對於內部嵌套的類型並不做處理。
這裏想處理深層嵌套屬性,就必須用到類型遞歸
結合 JavaScript 的處理方式,我們可以得到 TypeScript 的處理方式
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P]
};
判斷屬性值類型是否爲對象類型,是則遞歸類型轉化操作。
看一下轉化結果
type requiredUser = DeepRequired<IUser>
/* type requiredUser = {
name: string;
age: number;
sex: string;
department: string;
address: string;
company: DeepRequired<ICompany>;
} */
注意:
如果 IUser
中的 company
爲可選屬性
,那麼經過 DeepRequired<IUser>
轉化後,依然爲 company: ICompany; // 嵌套類型中的可選類型未被轉爲必選屬性
。
經過排查不難發現,當 company
爲可選屬性,那麼其對應的類型爲 ICompany | undefined
,此時可以得到 ICompany | undefined extends object
,並不滿足賦值條件,故而得到 false
分支,即(ICompany)。
特殊關鍵字
其中 +
、 -
這兩個關鍵字用於映射類型中給屬性添加修飾符,比如我們再上文中 Required<T>
的定義
type Required<T> = {
[P in keyof T]-?: T[P];
};
type RemoveReadonly<T> = {
-readonly [P in keyof T]: T[P]
}
用到了 -?
,它代表將可選屬性變爲必選,-readonly
代表將只讀屬性變爲非只讀。
註釋
我們可以使用 JSDoc
的方式來添加註釋,藉助於 IDE 可以給我們提供更友好的提示
enum EStatus {
/**
* 成功狀態
*/
Success,
/**
* 失敗狀態
*/
Error
}
// 在調用對應狀態的時候,會給出友好的文字提示
is 關鍵字
is
關鍵字經常用於依賴布爾值的判斷來縮小參數的類型範圍
比如:
function isString(test: any): test is string{
return typeof test === 'string';
}
function getStringLength(foo: number | string){
if(isString(foo)){
console.log(foo.length); // string function
}
}
getStringLength
在執行時,當 isString
返回 true
時,可以明確知道 foo
是字符串類型,故而調用 length
屬性正常運行。
然而用下面這種方式則會報錯
function isString(test: any): boolean{
return typeof test === 'string';
}
經過該函數的判斷並不能明確 foo
的類型,故而調用 length
報錯。
泛型約束
在使用泛型的過程中,我們的泛型幾乎可以是任何類型
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
如果我們可以明確傳入的泛型是屬於哪一類,那麼應用泛型約束將會使得代碼有更好可維護性。
比如,我們知道上面代碼中的泛型 T
屬於 object
類型,那麼對其添加泛型約束如下:
type ObjectReadonly<T extends object> = {
readonly [P in keyof T]: T[P];
};
這樣當我們在使用 工具類型 ObjectReadonly<T>
時,將會限制我們只能傳入 object
類型。