1. 程式人生 > 實用技巧 >理解 TypeScript 型別收窄

理解 TypeScript 型別收窄

一、型別收窄

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