詳解TypeScript映射類型和更好的字面量類型推斷
概述
TypeScript 2.1 引入了映射類型,這是對(duì)類型系統(tǒng)的一個(gè)強(qiáng)大的補(bǔ)充。本質(zhì)上,映射類型允許w咱們通過映射屬性類型從現(xiàn)有類型創(chuàng)建新類型。根據(jù)咱們指定的規(guī)則轉(zhuǎn)換現(xiàn)有類型的每個(gè)屬性。轉(zhuǎn)換后的屬性組成新的類型。
使用映射類型,可以捕獲類型系統(tǒng)中類似Object.freeze()等方法的效果。凍結(jié)對(duì)象后,就不能再添加、更改或刪除其中的屬性。來看看如何在不使用映射類型的情況下在類型系統(tǒng)中對(duì)其進(jìn)行編碼:
interface Point { x: number; y: number; } interface FrozenPoint { readonly x: number; readonly y: number; } function freezePoint(p: Point): FrozenPoint { return Object.freeze(p); } const origin = freezePoint({ x: 0, y: 0 }); // Error! Cannot assign to 'x' because it // is a constant or a read-only property. origin.x = 42;
咱們定義了一個(gè)包含x和y兩個(gè)屬性的Point接口,咱們還定義了另一個(gè)接口FrozenPoint,它與Point相同,只是它的所有屬性都被使用readonly定義為只讀屬性。
freezePoint函數(shù)接受一個(gè)Point作為參數(shù)并凍結(jié)該參數(shù),接著,向調(diào)用者返回相同的對(duì)象。然而,該對(duì)象的類型已更改為FrozenPoint,因此其屬性被靜態(tài)類型化為只讀。這就是為什么當(dāng)試圖將42賦值給x屬性時(shí),TypeScript會(huì)出錯(cuò)。在運(yùn)行時(shí),分配要么拋出一個(gè)類型錯(cuò)誤(嚴(yán)格模式),要么靜默失敗(非嚴(yán)格模式)。
雖然上面的示例可以正確地編譯和工作,但它有兩大缺點(diǎn)
- 需要兩個(gè)接口。除了Point類型之外,還必須定義FrozenPoint類型,這樣才能將readonly修飾符添加到兩個(gè)屬性中。當(dāng)咱們更改Point時(shí),還必須更改FrozenPoint,這很容易出錯(cuò),也很煩人。
- 需要 freezePoint函數(shù)。對(duì)于希望在應(yīng)用程序中凍結(jié)的每種類型的對(duì)象,咱們就必須定義一個(gè)包裝器函數(shù),該函數(shù)接受該類型的對(duì)象并返回凍結(jié)類型的對(duì)象。沒有映射類型,咱們就不能以通用的方式靜態(tài)地使用Object.freeze()。
使用映射類型構(gòu)建 Object.freeze()
來看看Object.freeze()是如何在lib.d.ts文件中定義的:
/** * Prevents the modification of existing property attributes and values, and prevents the addition of new properties. * @param o Object on which to lock the attributes. */ freeze<T>(o: T): Readonly<T>;
該方法的返回類型為Readonly<T>,這是一個(gè)映射類型,它的定義如下:
type Readonly<T> = { readonly [P in keyof T]: T[P] };
這個(gè)語法一開始可能會(huì)讓人望而生畏,咱們來一步一步分析它:
- 用一個(gè)名為T的類型參數(shù)定義了一個(gè)泛型 Readonly。
- 在方括號(hào)中,使用了keyof操作符。keyof T將T類型的所有屬性名表示為字符串字面量類型的聯(lián)合。
- 方括號(hào)中的in關(guān)鍵字表示我們正在處理映射類型。[P in keyof T]: T[P]表示將T類型的每個(gè)屬性P的類型轉(zhuǎn)換為T[P]。如果沒有readonly修飾符,這將是一個(gè)身份轉(zhuǎn)換。
- 類型T[P]是一個(gè)查找類型,它表示類型T的屬性P的類型。
- 最后,readonly修飾符指定每個(gè)屬性都應(yīng)該轉(zhuǎn)換為只讀屬性。
因?yàn)镽eadonly<T>類型是泛型的,所以咱們?yōu)門提供的每種類型都正確地入了Object.freeze()中。
const origin = Object.freeze({ x: 0, y: 0 }); // Error! Cannot assign to 'x' because it // is a constant or a read-only property. origin.x = 42;
映射類型的語法更直觀解釋
這次咱們使用Point類型為例來粗略解釋類型映射如何工作。請(qǐng)注意,以下只是出于解釋目的,并不能準(zhǔn)確反映TypeScript使用的解析算法。
從類型別名開始:
type ReadonlyPoint = Readonly<Point>;
現(xiàn)在,咱們可以在Readonly<T>中為泛型類型T的替換Point類型:
type ReadonyPoint = { readonly [P in keyof Point]: Point[P] };
現(xiàn)在咱們知道T是Point,可以確定keyof Point表示的字符串字面量類型的并集:
type ReadonlyPoint = { readonly [P in "x" | "y"]: Point[p] };
類型P表示每個(gè)屬性x和y,咱們把它們作為單獨(dú)的屬性來寫,去掉映射的類型語法
type ReadonlyPoint = { readonly x: Point["x"]; readonly y: Point["y"]; };
最后,咱們可以解析這兩種查找類型,并將它們替換為具體的x和y類型,這兩種類型都是number。
type ReadonlyPoint = { readonly x: number; readonly y: number; };
最后,得到的ReadonlyPoint類型與咱們手動(dòng)創(chuàng)建的FrozenPoint類型相同。
更多映射類型的示例
上面已經(jīng)看到lib.d.ts文件中內(nèi)置的Readonly <T>類型。此外,TypeScript 定義了其他映射類型,這些映射類型在各種情況下都非常有用。如下:
/** * Make all properties in T optional */ type Partial<T> = { [P in keyof T]?: T[P] }; /** * From T pick a set of properties K */ type Pick<T, K extends keyof T> = { [P in K]: T[P] }; /** * Construct a type with a set of properties K of type T */ type Record<K extends string, T> = { [P in K]: T };
這里還有兩個(gè)關(guān)于映射類型的例子,如果需要的話,可以自己編寫:
/** * Make all properties in T nullable */ type Nullable<T> = { [P in keyof T]: T[P] | null }; /** * Turn all properties of T into strings */ type Stringify<T> = { [P in keyof T]: string };
映射類型和聯(lián)合的組合也是很有趣:
type X = Readonly<Nullable<Stringify<Point>>>; // type X = { // readonly x: string | null; // readonly y: string | null; // };
映射類型的實(shí)際用例
實(shí)戰(zhàn)中經(jīng)常可以看到映射類型,來看看react和 Lodash :
- react:組件的setState方法允許咱們更新整個(gè)狀態(tài)或其中的一個(gè)子集。咱們可以更新任意多個(gè)屬性,這使得setState方法成為Partial<T>的一個(gè)很好的用例。
- Lodash:pick函數(shù)從一個(gè)對(duì)象中選擇一組屬性。該方法返回一個(gè)新對(duì)象,該對(duì)象只包含咱們選擇的屬性。可以使用Pick<T>對(duì)該行為進(jìn)行構(gòu)建,正如其名稱所示。
更好的字面量類型推斷
字符串、數(shù)字和布爾字面量類型(如:"abc",1和true)之前僅在存在顯式類型注釋時(shí)才被推斷。從 TypeScript 2.1 開始,字面量類型總是推斷為默認(rèn)值。在 TypeScript 2.0 中,類型系統(tǒng)擴(kuò)展了幾個(gè)新的字面量類型:
- boolean字面量類型
- 數(shù)字字面量
- 枚舉字面量
不帶類型注解的const變量或readonly屬性的類型推斷為字面量初始化的類型。已經(jīng)初始化且不帶類型注解的let變量、var變量、形參或非readonly屬性的類型推斷為初始值的擴(kuò)展字面量類型。字符串字面量擴(kuò)展類型是string,數(shù)字字面量擴(kuò)展類型是number,true或false的字面量類型是boolean,還有枚舉字面量擴(kuò)展類型是枚舉。
更好的 const 變量推斷
咱們從局部變量和var關(guān)鍵字開始。當(dāng)TypeScript看到下面的變量聲明時(shí),它會(huì)推斷baseUrl變量的類型是string:
var baseUrl = "https://example.com/"; // 推斷類型: string
用let關(guān)鍵字聲明的變量也是如此
let baseUrl = "https://example.com/"; // 推斷類型: string
這兩個(gè)變量都推斷為string類型,因?yàn)樗鼈兛梢噪S時(shí)更改。它們是用一個(gè)字面量字符串值初始化的,但是以后可以修改它們。
但是,如果使用const關(guān)鍵字聲明變量并使用字符串字面量進(jìn)行初始化,則推斷的類型不再是string,而是字面量類型:
const baseUrl = "https://example.com/"; // 推斷類型: "https://example.com/"
由于常量字符串變量的值永遠(yuǎn)不會(huì)改變,因此推斷出的類型會(huì)更加的具體。baseUrl變量無法保存"https://example.com/"以外的任何其他值。
字面量類型推斷也適用于其他原始類型。如果用直接的數(shù)值或布爾值初始化常量,推斷出的還是字面量類型:
const HTTPS_PORT = 443; // 推斷類型: 443 const rememberMe = true; // 推斷類型: true
類似地,當(dāng)初始化器是枚舉值時(shí),推斷出的也是字面量類型:
enum FlexDirection { Row, Column } const direction = FlexDirection.Column; // 推斷類型: FlexDirection.Column
注意,direction類型為FlexDirection.Column,它是枚舉字面量類型。如果使用let或var關(guān)鍵字來聲明direction變量,那么它的推斷類型應(yīng)該是FlexDirection。
更好的只讀屬性推斷
與局部const變量類似,帶有字面量初始化的只讀屬性也被推斷為字面量類型:
class ApiClient { private readonly baseUrl = "https://api.example.com/"; // 推斷類型: "https://api.example.com/" get(endpoint: string) { // ... } }
只讀類屬性只能立即初始化,也可以在構(gòu)造函數(shù)中初始化。試圖更改其他位置的值會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤。因此,推斷只讀類屬性的字面量類型是合理的,因?yàn)樗闹挡粫?huì)改變。
當(dāng)然,TypeScript 不知道在運(yùn)行時(shí)發(fā)生了什么:用readonly標(biāo)記的屬性可以在任何時(shí)候被一些js代碼改變。readonly修飾符只限制從TypeScript代碼中對(duì)屬性的訪問,在運(yùn)行時(shí)就無能為力。也就是說,它會(huì)被編譯時(shí)刪除掉,不會(huì)出現(xiàn)在生成的js代碼中。
推斷字面量類型的有用性
你可能會(huì)問自己,為什么推斷const變量和readonly屬性為字面量類型是有用的。考慮下面的代碼:
const HTTP_GET = "GET"; // 推斷類型: "GET" const HTTP_POST = "POST"; // 推斷類型: "POST" function get(url: string, method: "GET" | "POST") { // ... } get("https://example.com/", HTTP_GET);
如果推斷HTTP_GET常量的類型是string而不是“GET”,則會(huì)出現(xiàn)編譯時(shí)錯(cuò)誤,因?yàn)闊o法將HTTP_GET作為第二個(gè)參數(shù)傳遞給get函數(shù):
Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'
當(dāng)然,如果相應(yīng)的參數(shù)只允許兩個(gè)特定的字符串值,則不允許將任意字符串作為函數(shù)參數(shù)傳遞。但是,當(dāng)為兩個(gè)常量推斷字面量類型“GET”和“POST”時(shí),一切就都解決了。
以上就是詳解TypeScript映射類型和更好的字面量類型推斷的詳細(xì)內(nèi)容,更多關(guān)于TS的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript實(shí)現(xiàn)鼠標(biāo)拖拽效果
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)鼠標(biāo)拖拽效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10JavaScript學(xué)習(xí)筆記之基于定時(shí)器實(shí)現(xiàn)圖片無縫滾動(dòng)功能詳解
這篇文章主要介紹了JavaScript學(xué)習(xí)筆記之基于定時(shí)器實(shí)現(xiàn)圖片無縫滾動(dòng)功能,結(jié)合實(shí)例形式分析了javascript定時(shí)器與頁面元素屬性動(dòng)態(tài)設(shè)置等相關(guān)操作技巧,需要的朋友可以參考下2019-01-01JS實(shí)現(xiàn)簡單網(wǎng)頁倒計(jì)時(shí)器
這篇文章主要為大家詳細(xì)介紹了JS實(shí)現(xiàn)簡單網(wǎng)頁倒計(jì)時(shí)器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08input獲取焦點(diǎn)時(shí)底部菜單被頂上來問題的解決辦法
這篇文章主要介紹了解決input獲取焦點(diǎn)時(shí)底部菜單被頂上來問題的方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的的朋友參考下2017-01-01javascript 3d 逐偵產(chǎn)品展示(核心精簡)
這篇文章主要介紹了javascript實(shí)現(xiàn)的3d逐偵產(chǎn)品展示,需要的朋友可以參考下2014-03-03javascript校驗(yàn)價(jià)格合法性實(shí)例(必須輸入2位小數(shù))
這篇文章主要介紹了javascript校驗(yàn)價(jià)格合法性實(shí)例,其中價(jià)格必須是數(shù)字且必須輸入2位小數(shù),需要的朋友可以參考下2014-05-05