Tackling TypeScript
請支持這本書:購買捐款
(廣告,請不要封鎖。)

21 類型斷言(與轉型相關)



本章討論 TypeScript 中的類型斷言,這與其他語言中的類型轉換相關,並透過 as 運算子執行。

21.1 類型斷言

類型斷言讓我們可以覆寫 TypeScript 為值計算出的靜態類型。這對於解決類型系統的限制很有用。

類型斷言與其他語言中的類型轉換相關,但它們不會擲回例外,也不會在執行階段執行任何動作(它們會在靜態時執行一些基本檢查)。

const data: object = ['a', 'b', 'c']; // (A)

// @ts-expect-error: Property 'length' does not exist on type 'object'.
data.length; // (B)

assert.equal(
  (data as Array<string>).length, 3); // (C)

註解

類型斷言是最後的選擇,應盡量避免。它們(暫時)移除了靜態類型系統通常給我們的安全網。

請注意,在 A 行中,我們也覆寫了 TypeScript 的靜態類型。但我們透過類型註解來執行此動作。這種覆寫方式比類型斷言安全得多,因為我們的限制更多:TypeScript 的類型必須可以指定給註解的類型。

21.1.1 類型斷言的替代語法

TypeScript 有類型斷言的替代「尖括號」語法

<Array<string>>data

我建議避免使用此語法。它已經過時,且與 React JSX 程式碼(在 .tsx 檔案中)不相容。

21.1.2 範例:宣告介面

為了存取任意物件 obj 的屬性 .name,我們暫時將 obj 的靜態類型變更為 Named(A 行和 B 行)。

interface Named {
  name: string;
}
function getName(obj: object): string {
  if (typeof (obj as Named).name === 'string') { // (A)
    return (obj as Named).name; // (B)
  }
  return '(Unnamed)';
}

21.1.3 範例:宣告索引特徵

在下列程式碼中(A 行),我們使用類型宣告 as Dict,以便存取推論類型為 object 的值的屬性。也就是說,我們使用靜態類型 Dict 覆寫靜態類型 object

type Dict = {[k:string]: any};

function getPropertyValue(dict: unknown, key: string): any {
  if (typeof dict === 'object' && dict !== null && key in dict) {
    // %inferred-type: object
    dict;

    // @ts-expect-error: Element implicitly has an 'any' type because
    // expression of type 'string' can't be used to index type '{}'.
    // [...]
    dict[key];
    
    return (dict as Dict)[key]; // (A)
  } else {
    throw new Error();
  }
}

21.2.1 非空斷言運算子(後綴 !

如果值的類型是包含類型 undefinednull 的聯集,非空斷言運算子(或非空斷言運算子)會從聯集中移除這些類型。我們告訴 TypeScript:「這個值不能是 undefinednull。」因此,我們可以執行這些兩個值類型所禁止的運算,例如

const theName = 'Jane' as (null | string);

// @ts-expect-error: Object is possibly 'null'.
theName.length;

assert.equal(
  theName!.length, 4); // OK
21.2.1.1 範例 – Maps:在 .has() 之後的 .get()

在我們使用 Map 方法 .has() 之後,我們知道 Map 有給定的金鑰。唉,.get() 的結果並未反映該知識,這就是我們必須使用非空斷言運算子的原因

function getLength(strMap: Map<string, string>, key: string): number {
  if (strMap.has(key)) {
    // We are sure x is not undefined:
    const value = strMap.get(key)!; // (A)
    return value.length;
  }
  return -1;
}

如果 Map 的值不能是 undefined,我們可以避免使用非空斷言運算子。然後,可以透過檢查 .get() 的結果是否為 undefined 來偵測遺失的項目

function getLength(strMap: Map<string, string>, key: string): number {
  // %inferred-type: string | undefined
  const value = strMap.get(key);
  if (value === undefined) { // (A)
    return -1;
  }

  // %inferred-type: string
  value;

  return value.length;
}

21.2.2 明確指定宣告

如果已開啟嚴格屬性初始化,我們偶爾需要告訴 TypeScript 我們確實初始化某些屬性,即使它認為我們沒有。

這是 TypeScript 抱怨的範例,即使它不應該這樣做

class Point1 {
  // @ts-expect-error: Property 'x' has no initializer and is not definitely
  // assigned in the constructor.
  x: number;

  // @ts-expect-error: Property 'y' has no initializer and is not definitely
  // assigned in the constructor.
  y: number;

  constructor() {
    this.initProperties();
  }
  initProperties() {
    this.x = 0;
    this.y = 0;
  }
}

如果我們在 A 行和 B 行使用明確指定宣告(驚嘆號),錯誤就會消失

class Point2 {
  x!: number; // (A)
  y!: number; // (B)
  constructor() {
    this.initProperties();
  }
  initProperties() {
    this.x = 0;
    this.y = 0;
  }
}