一、類型收窄
TypeScript 類型收窄就是從寬類型轉換成窄類型的過程。類型收窄常用於處理聯合類型變量的場景,一個常見的例子是非空檢查:
// Type is htmlElement | null const el = document.getElementById("foo"); if (el) { // Type is htmlElement el.innerHTML = "semlinker"; } else { // Type is null alert("id為foo的元素不存在"); }
如果 el 為 null,則第一個分支中的代碼將不會執行。因此,TypeScript 能夠從此代碼塊內的聯合類型中排除 null 類型,從而產生更窄的類型,更易於使用。
此外,你還可以通過拋出異常或從分支返回,來收窄變量的類型。例如:
// Type is HTMLElement | null const el = document.getElementById("foo"); if (!el) throw new Error("找不到id為foo的元素"); // Type is HTMLElement el.innerHTML = "semlinker";
其實在 TypeScript 中,有許多方法可以收窄變量的類型。比如使用 instanceof 運算符:
function contains(text: string, search: string | RegExp) { if (search instanceof RegExp) { // Type is RegExp return !!search.exec(text); } // Type is string return text.includes(search); }
當然屬性檢查也是可以的:
interface Admin {
name: string; privileges: string[]; } interface Employee { name: string; startDate: Date; } type UnknownEmployee = Employee | Admin; function printEmployeeInformation(emp: UnknownEmployee) { console.log("Name: " + emp.name); if ("privileges"in emp) { // Type is Admin console.log("Privileges: " + emp.privileges); } if ("startDate"in emp) { // Type is Employee console.log("Start Date: " + emp.startDate); } }
使用一些內置的函數,比如 Array.isArray 也能夠收窄類型:
function contains(text: string, terms: string | string[]) { const termList = Array.isArray(terms) ? terms : [terms]; termList; // Type is string[] // ... }
一般來說 TypeScript 非常擅長通過條件來判別類型,但在處理一些特殊值時要特別注意 —— 它可能包含你不想要的東西!例如,以下從聯合類型中排除 null 的方法是錯誤的:
const el = document.getElementById("foo"); // Type is HTMLElement | null if (typeof el === "object") { el; // Type is HTMLElement | null }
因為在 JavaScript 中 typeof null 的結果是 “object” ,所以你實際上並沒有通過這種檢查排除 null 值。除此之外,falsy 的原始值也會產生類似的問題:
function foo(x?: number | string | null) { if (!x) { x; // Type is string | number | null | undefined } }
因為空字符串和 0 都屬於 falsy 值,所以在分支中 x 的類型可能是 string 或 number 類型。幫助類型檢查器縮小類型的另一種常見方法是在它們上放置一個明確的 “標簽”:
interface UploadEvent { type: "upload"; filename: string; contents: string; } interface DownloadEvent { type: "download"; filename: string; } type AppEvent = UploadEvent | DownloadEvent; function handleEvent(e: AppEvent) { switch (e.type) { case "download": e; // Type is DownloadEvent break; case "upload": e; // Type is UploadEvent break; } }
這種模式也被稱為 ”標簽聯合“ 或 ”可辨識聯合“,它在 TypeScript 中的應用范圍非常廣。
如果 TypeScript 不能識別出類型,你甚至可以引入一個自定義函數來幫助它:
function isInputElement(el: HTMLElement): el is HTMLInputElement { return "value" in el; } function getElementContent(el: HTMLElement) { if (isInputElement(el)) { // Type is HTMLInputElement return el.value; } // Type is HTMLElement return el.textContent; }
這就是所謂的 “用戶定義類型保護”。 el is HTMLInputElement ,作為返回類型告訴類型檢查器,如果函數返回true,則 el 變量的類型就是 HTMLInputElement。
類型保護是可執行運行時檢查的一種表達式,用於確保該類型在一定的范圍內。換句話說,類型保護可以保證一個字符串是一個字符串,盡管它的值也可以是一個數值。類型保護與特性檢測並不是完全不同,其主要思想是嘗試檢測屬性、方法或原型,以確定如何處理值。
一些函數能夠使用類型保護來執行數組或對象的類型收窄。例如,如果你在一個數組中進行一些查找,你可能會得到一個 nullable 類型的數組:
const supermans = ["Qinhw", "Pingan8787", "Semlinker", "Kaquko", "Lolo"]; const members = ["Semlinker", "Lolo"] .map((who) => supermans.find((n) => n === who)) // Type is (string | undefined)[]
這時你可能想到使用 filter 方法過濾掉未定義的值:
const supermans = ["Qinhw", "Pingan8787", "Semlinker", "Kaquko", "Lolo"]; const members = ["Semlinker", "Lolo"] .map((who) => supermans.find((n) => n === who)) .filter((who) => who !== undefined); // Type is (string | undefined)[]
可惜的是 TypeScript 也無法理解你的意圖,但是如果你使用一個類型保護函數的話就可以:
function isDefined<T>(x: T | undefined): x is T { return x !== undefined; } const supermans = ["Qinhw", "Pingan8787", "Semlinker", "Kaquko", "Lolo"]; const members = ["Semlinker", "Lolo"] .map((who) => supermans.find((n) => n === who)) .filter(isDefined); // Type is string[]
二、全面性檢查
在 TypeScript 中我們可以利用類型收窄和 never 類型的特性來全面性檢查,比如:
type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) { if(typeof foo === "string") { // 這里 foo 被收窄為 string 類型 } else if(typeof foo === "number") { // 這里 foo 被收窄為 number 類型 } else { // foo 在這里是 never const check: never = foo; } }
注意在 else 分支里面,我們把收窄為 never 的 foo 賦值給一個顯示聲明的 never 變量。如果一切邏輯正確,那么這里應該能夠編譯通過。但是假如后來有一天你的同事修改了 Foo 的類型:
type Foo = string | number | boolean;
然而他忘記同時修改 controlFlowAnalysisWithNever 方法中的控制流程,這時候 else 分支的 foo 類型會被收窄為 boolean 類型,導致無法賦值給 never 類型,這時就會產生一個編譯錯誤。通過這個方式,我們可以確保
controlFlowAnalysisWithNever 方法總是窮盡了 Foo 的所有可能類型。 通過這個示例,我們可以得出一個結論: 使用 never 避免出現新增了聯合類型沒有對應的實現,目的就是寫出類型絕對安全的代碼。
三、總結
理解 TypeScript 中的類型收窄將幫助你建立一個關於類型推斷如何工作的認知,進一步理解錯誤,它通常與類型檢查器有更緊密的聯系。
Dan Vanderkam 大神寫的 ”62 Specific Ways to Improve Your TypeScript“ 這本書內容挺不錯的,有興趣的讀者可以閱讀一下。
品牌vi設計公司http://www.maiqicn.com 辦公資源網站大全https://www.wode007.com