深入了解TypeScript中的映射類型
DRY 原則(Don't repeat yourself)是軟件開發(fā)中最重要的原則之一,即不要重復自己。應該避免在代碼中的兩個或多個地方存在重復的業(yè)務邏輯。
在 TypeScript 中,映射類型可以幫助我們避免編寫重復的代碼,它可以根據(jù)現(xiàn)有類型和定義的一些規(guī)則來創(chuàng)建新類型。下面就來看一下什么是映射類型以及如何構建自己的映射類型。
1. 基本概念
在介紹映射類型之前,先來看一些前置知識。
(1)索引訪問類型
在 TypeScript 中,我們可以通過按名稱查找屬性來訪問它的類型:
type?AppConfig?=?{
??username:?string;
??layout:?string;
};
type?Username?=?AppConfig["username"];
在這個例子中,通過 AppConfig 類型的索引 username 獲取到其類型 string,類似于在 JavaScript 中通過索引來獲取對象的屬性值。
(2)索引簽名
當類型屬性的實際名稱是未知的,但它們將引用的數(shù)據(jù)類型已知時,索引簽名就很方便。
type?User?=?{
??name:?string;
??preferences:?{
????[key:?string]:?string;
??}
};
const?currentUser:?User?=?{
??name:?'Foo?Bar',
??preferences:?{
????lang:?'en',
??},
};
const?currentLang?=?currentUser.preferences.lang;
在上面的例子中,currentLang 的類型是 string 而不是 any。此功能與 keyof 運算符一起搭配使用是使映射類型成為可能的核心之一。
(3)聯(lián)合類型
聯(lián)合類型是兩種或多種類型的組合。它表明值的類型可以是聯(lián)合中包含的任何一種類型。
type?StringOrNumberUnion?=?string?|?number; let?value:?StringOrNumberUnion?=?'hello,?world!'; value?=?100;
下面是一個更復雜的例子,編譯器可以為聯(lián)合類型提供一些高級保護:
type?Animal?=?{
??name:?string;
??species:?string;
};
type?Person?=?{
??name:?string;
??age:?number;
};
type?AnimalOrPerson?=?Animal?|?Person;
const?value:?AnimalOrPerson?=?loadFromSomewhereElse();
console.log(value.name);???//??
console.log(value.age);????//??
if?('age'?in?value)?{
??console.log(value.age);?//??
}
在這個例子中,因為 Animal 和 Person 都有 name 屬性,所以第 15 行的 value.name 可以正常輸出,沒有錯誤。而第 16 行的 value.age 會編譯錯誤,因為如果 value 是 Animal 類型,則 value 是沒有 age 屬性的。在第 19 行的 if 塊中,因為只有 value 存在 age 屬性才能進入這個代碼塊。所以,在這個 if 塊中,value 一定是 Person,TS 可以知道 value 一定是具有 age 屬性的,所以編譯正確。
(4)keyof 類型運算符
keyof 類型運算符返回傳遞給它的類型的 key 的聯(lián)合。
type?AppConfig?=?{
??username:?string;
??layout:?string;
};
type?AppConfigKey?=?keyof?AppConfig;
在這個例子中,AppConfigKey 類型會被解析為"username" | "layout"。它可以與索引簽名一起使用:
type?User?=?{
??name:?string;
??preferences:?{
????[key:?string]:?string;
??}
};
type?UserPreferenceKey?=?keyof?User["preferences"];
這里,UserPreferenceKey 類型被解析為 string | number。
(5)元組類型
元組是一種特殊的數(shù)組類型,其中數(shù)組的元素可能是特定索引處的特定類型。它們允許 TypeScript 編譯器圍繞值數(shù)組提供更高的安全性,尤其是當這些值屬于不同類型時。
例如,TypeScript 編譯器能夠為元組的各種元素提供類型安全:
type?Currency?=?[number,?string];
const?amount:?Currency?=?[100,?'USD'];
function?add(values:?number[])?{
???return?values.reduce((a,?b)?=>?a?+?b);
}
add(amount);
//?Error:?Argument?of?type?'Currency'?is?not?assignable?to?parameter?of?type?'number[]'.
//?Type?'string'?is?not?assignable?to?type?'number'.
上面的代碼中會報錯,Currency 類型的參數(shù)不能分配給“number[]”類型的參數(shù),string 類型不能分配給 number 類型。
當訪問超出元組定義類型的索引處的元素時,TypeScript 能夠進行提示:
type?LatLong?=?[number,?number];? const?loc:?LatLong?=?[48.858370,?2.294481]; console.log(loc[2]); //?Error:?Tuple?type?'LatLong'?of?length?'2'?has?no?element?at?index?'2'.
這里,元組類型 LatLong 只有兩個元素,當試圖訪問第三個元素時,就會報錯。
(6)條件類型
條件類型是一個表達式,類似于 JavaScript 中的三元表達式,其語法如下:
T?extends?U???X?:?Y
來看一個實際的例子:
type?ConditionalType?=?string?extends?boolean???string?:?boolean;
在上面的示例中,ConditionalType 的類型將是 boolean,因為條件string extends boolean 是始終為 false。
2. 映射類型
(1)初體驗
在 TypeScript 中,當需要從另一種類型派生(并保持同步)另一種類型時,使用映射類型會特別有用。
//?用戶的配置值
type?AppConfig?=?{
??username:?string;
??layout:?string;
};
//?用戶是否有權更改配置值
type?AppPermissions?=?{
??changeUsername:?boolean;
??changeLayout:?boolean;
};
在上面的代碼中,AppConfig 和 AppPermissions 之間是存在隱式關系的,每當向 AppConfig 添加新的配置值時,AppPermissions 中也必須有相應的布爾值。
這里可以使用映射類型來管理兩者之間的關系:
type?AppConfig?=?{
??username:?string;
??layout:?string;
};
type?AppPermissions?=?{
??[Property?in?keyof?AppConfig?as?`change${Capitalize<Property>}`]:?boolean
};
在上面的代碼中,只要 AppConfig 中的類型發(fā)生變化,AppPermissions 就會隨之變化。實現(xiàn)了兩者之間的映射關系。
(2)概念
在 TypeScript 和 JavaScript 中,最常見的映射就是 Array.prototype.map():
[1,?2,?3].map(value?=>?value.toString());?//?["1",?"2",?"3"]
這里,我們將數(shù)組中的數(shù)字映射到其字符串的表示形式。因此,TypeScript 中的映射類型意味著將一種類型轉換為另一種類型,方法就是對其每個屬性進行轉換。
(3)實例
下面來通過一個例子來深入理解一下映射類型。對設備定義以下類型,其包含制造商和價格屬性:
type?Device?=?{
??manufacturer:?string;
??price:?number;
};
為了讓用戶更容易理解設備信息,因此為對象添加一個新類型,該對象可以使用適當?shù)母袷絹砀袷交O備的每個屬性:
type?DeviceFormatter?=?{
??[Key?in?keyof?Device?as?`format${Capitalize<Key>}`]:?(value:?Device[Key])?=>?string;
};
我們來拆解一下上面的代碼。Key in keyof Device 使用 keyof 類型運算符生成 Device 中所有鍵的并集。將它放在索引簽名中實際上是遍歷 Device 的所有屬性并將它們映射到 DeviceFormatter 的屬性。
format${Capitalize<Key>} 是映射的轉換部分,它使用 key 重映射和模板文字類型將屬性名稱從 x 更改為 formatX。
(value: Device[Key]) => string; 利用索引訪問類型 Device[Key] 來指示格式化函數(shù)的 value 參數(shù)是格式化的屬性的類型。因此,formatManufacturer 接受一個 string(制造商),而 formatPrice 接受一個number(價格)。
下面是 DeviceFormatter 類型的樣子:
type?DeviceFormatter?=?{
??formatManufacturer:?(value:?string)?=>?string;
??formatPrice:?(value:?number)?=>?string;
};
現(xiàn)在,假設將第三個屬性 releaseYear 添加到 Device 類型中:
type?Device?=?{
??manufacturer:?string;
??price:?number;
??releaseYear:?number;
}
由于映射類型的強大功能,DeviceFormatter 類型會自動擴展為如下類型,無需進行任何額外的工作:
type?DeviceFormatter?=?{
??formatManufacturer:?(value:?string)?=>?string;
??formatPrice:?(value:?number)?=>?string;
??formatReleaseYear:?(value:?number)?=>?string;
};
3. 實用程序中的映射
TypeScript 附帶了許多用作實用程序的映射類型,最常見的包括 Omit、Partial、Readonly、Readonly、Exclude、Extract、NonNullable、ReturnType 等。下面來看看其中的兩個是如何構建的。
(1)Partial
Partial 是一種映射類型,可以將已有的類型屬性轉換為可選類型,并通過使用與 undefined 的聯(lián)合使類型可以為空。
interface?Point3D?{
????x:?number;
????y:?number;
????z:?number;
}
type?PartialPoint3D?=?Partial<Point3D>;
這里的 PartialPoint3D 類型實際是這樣的:
type?PartialPoint3D?=?{
????x?:?number;
????y?:?number;
????z?:?number;
}
當我們鼠標懸浮在 Partial 上時,就會看到它的定義:

把它拿出來:
type?Partial<T>?=?{?[P?in?keyof?T]?:?T[P]?|?undefined;?}
下面來拆解一下這行代碼:
- 使用泛型來傳遞目標接口 T;
- 使用
keyof T來獲取 T 的所有 key。 - 通過使用
[P in keyof T]來訪問并循環(huán)所有的 key; - 它通過添加 ? 使 key 成為可選的。
- 使用聯(lián)合類型
T[P] | undefined使 key 的類型可以為空;
(2)Exclude
Exclude 是一種映射類型,可讓有選擇地從類型中刪除屬性。其定義如下:
type?Exclude<T,?U>?=?T?extends?U???never?:?T
它通過使用條件類型從 T 中排除那些可分配給 U 的類型,并且在排除的屬性上返回 nerver。
type?animals?=?'bird'?|?'cat'?|?'crocodile'; type?mamals?=?Exclude<animals,?'crocodile'>;??//?'bird'?|?'cat'
4. 構建映射類型
通過上面的對 TypeScript 內置實用程序類型的原理解釋,對映射類型有了更深的理解。最后,我們來構建一個自己的映射類型:Optional,它可以將原類型中指定 key 的類型置為可選的并且可以為空。
我們可以這樣做:
- 將整個類型轉換為 Optional
- 從該新類型中僅選擇想要的屬性使其成為可選的。
- 將原始類型與排除的屬性連接起來。
實現(xiàn)代碼及測試用例如下:
type?Optional<T,?K?extends?keyof?T>?=?Pick<Partial<T>,?K>?&?Omit<T,?K>;
type?Person?=?{
??name:?string;
??surname:?string;
??email:?string;
}
??
type?User?=?Optional<Person,?'email'>;
//?現(xiàn)在?email?屬性是可選的
type?AnonymousUser?=?Optional<Person,?'name'?|?'surname'>;
//?現(xiàn)在?email?和?surname?屬性是可選的
注意,這里使用 K extends keyof T 來確保只能傳遞屬于類型/接口的屬性。否則,TypeScript 將在編譯時拋出錯誤。
映射類型的一大優(yōu)點就是它們的可組合性:可以組合它們來創(chuàng)建新的映射類型。
上面使用了已有的實用程序類型實現(xiàn)了我們想要的 Optional。當然,我們也可以在不使用任何其他映射類型的情況下重新創(chuàng)建 Optional 映射類型實用程序:
type?Optional<T,?K?extends?keyof?T>?=
????{?[P?in?K]?:?T[P]?}
????&
????{?[P?in?Exclude<keyof?T,?K>]:?T[P]?};
上面的代碼結合了兩種類型:
- 第一種類型通過使用
?修飾符使 T 的所有 K 的 key 都是可選的。 - 第二種類型通過使用
Excluse<keyof T,K>來獲取剩余的key。
以上就是深入了解TypeScript中的映射類型的詳細內容,更多關于TypeScript映射類型的資料請關注腳本之家其它相關文章!
相關文章
一道面試題引發(fā)的對javascript類型轉換的思考
本文主要介紹了javascript類型轉換的相關知識,具有很好的參考價值。下面跟著小編一起來看下吧2017-03-03
JavaScript實現(xiàn)字符串與日期的互相轉換及日期的格式化
這篇文章主要介紹了JavaScript實現(xiàn)字符串與日期的互相轉換及日期的格式化的方法,這里格式化使用的是正則表達式來替換日期后進行格式化,需要的朋友可以參考下2016-03-03
JS實現(xiàn)動態(tài)增加和刪除li標簽行的實例代碼
下面小編就為大家?guī)硪黄狫S實現(xiàn)動態(tài)增加和刪除li標簽行的實例代碼。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-10-10

