[TypeScript] 聯集與字面

TypeScript 進行型別推斷的兩個關鍵概念:

  • 聯集(Unions):將資料的允許型別擴充為兩種或多種可能的型別。
  • 窄化(Narrowing):將資料的允許型別盡量減少。

聯集型別

例如以下程式:

let mathematician = Math.random() > 0.5 ? undefined : "HI";

mathematician 會是什麼型別?會是 string | undefined 聯集(Union)型別。

TypeScript 使用 | (管道) 運算符號,來表示資料可能的型別。

宣告聯集型別

以下範例,thinker 的初始資料為 null,但它的聯集型別為 string | null,所以後續也可以將字串資料,指派給 thinker 變數:

let thinker: string | null = null;
thinker = "Hi"; // 這是允許的

聯集屬性

例如以下程式:

let physicist = Math.random() > 0.5 ? "Hi" : 84; // 可得知 physicist 的聯集型別為 number | string。

physicist.toString(); // 這是允許的,因為 number 及 string 型別都有 toString() 函式。

physicist.toUpperCase(); // 這會報錯,因為 number 型別沒有 toUpperCase() 函式。

physicist.toFixed(); // 這會報錯,因為 string 型別沒有 toFixed() 函式。

限制對所有聯集型別存取不存在的屬性,是一種安全措施。

要使用僅存在於特定型別上的屬性時,我們需要向 TypeScript 提出更具體說明,這稱為窄化(narrowing)的過程。

窄化

窄化是指程式碼中,推斷出資料的型別,以及它的定義、宣告,使之更具體化。一旦 TypeScript 知道一個資料的型別比先前知道的更窄小,將允許我們將變數視為更具體的型別。可用於縮小型別的邏輯檢查,稱之為型別防護(type guard)

指派的窄化

如果直接為變數指派資料,TypeScript 會將變數的型別窄化到該資料的型別。例:

let admiral: number | string; // 聯集型別
admiral = "Hi";               // 窄化為 string 型別
admiral.toUpperCase();        // 是可以的

admiral.toFixed(); // 會報錯,因為型別 string 沒有 toFixed() 函式。

或者也可能是以下,宣告型為為 number | string,然後馬上指派資料,也會發生窄化的效果:

let inventor: number | string = "Hi"; // 窄化為 string
inventor.toUpperCase(); // 正確:型別為 string
inventor.toFixed();     // 報錯,型別 string 沒有 toFixed() 函式。

條件檢查的窄化

例如以下範例:

let scientist = Math.random() > 0.5 ? "Hi" : 51; // number | string

if(scientist === "Hi"){
  scientist.toUpperCase(); // 是可以的,因為前面 if 條件判斷的關係,所以 scientist 型別窄化為 string。
}

scientist.toUpperCase(); // 報錯,因為這裡的 scientist 型別為 number | string,在 number 型別中,沒有 toUpperCase() 函式。

使用條件邏輯窄化 TypeScript 的型別檢查,TypeScript 迫使我們安全地使用程式碼。

型別檢查的窄化

可以使用 typeof 關鍵字來窄化變數型別,例如:

let researcher = Math.random() > 0.5 ? "Hi" : 51; // number | string

if(typeof researcher === "string"){
  researcher.toUpperCase(); // 是可以的,型別為 string
}

// 邏輯的否定 ! 以及 else 語法也可以一併使用
if(!(typeof researcher === "string")){
  researcher.toFixed(); // 是可以的,型別為 number
}else{
  researcher.toUpperCase(); // 是可以的,型別為 string
}

// 三元運算子範例
typeof researcher === "string" ? researcher.toUpperCase() : researcher.toFixed(); // 這也是可以的

字面型別(literal types)

前面已瞭解了聯集型別和窄化的處理,接下來可透過字面型別(literal types):用以表達更具體的原始型別版本

例如以下這個範例:

const philosopher = "Hi";

乍看之下,沒錯,philosopher 的型別是 string。但也可以換另一種更具體的說法,可以說是 “Hi” 這個字面型別。

也就是如果將一個變數宣告為 const,並直接給一個固定資料,TypeScript 就會將該變數推斷為字面資料,視其為一種型別。例如下圖:

若是改用 let 來做變數的宣告,則會推斷為原始型別,如下例:

聯集型別註記

例如以下範例:

let lifespan: number | "ongoing" | "uncertain"; // 聯集型別 number | "ongoing" | "uncertain"

lifespan = 89;        // 是可以的
lifespan = "ongoing"; // 是可以的

lifespan = true;      // 這會報錯,因為 true 不可指派給型別 number | "ongoing" | "uncertain"

字面指派性

例如以下範例:

let specificallyAda: "Ada"; // 被宣告為字面型別「Ada」

specificallyAda = "Ada";    // 是可以的

specificallyAda = "Byron";  // 會報錯,型別 "Byron" 不可指派給型別 "Ada"。

嚴格的 null 檢查(strictNullChecks)

有一些程式語言,沒有嚴格檢查空值(null)的情況,會出現下面這樣將 null 指派給 string 的程式碼:

const firstName: string = null;

這種情況是非常不好的。

所以在 TypeScript 程式的設定檔當中,其中有一個選項是 strictNullChecks,該功能最好是要開啟(設定成 true),如果關閉的話,會讓 nullundefined 允許指派給變數,這非常不好。

真值(truthy)的窄化

JavaScript 中的所有值都是真值(truthy);有一些例外會被定義為假值(falsy),例:false、0、-0、0n、””、null、undefined 和 NaN。

TypeScript 還可以透過真假值檢查來窄化變數的型別,例如以下程式:

let geneticist = Math.random() > 0.5 ? "Hi" : undefined;

if(geneticist){
  geneticist.toUpperCase(); // 這是允許的,因為窄化了真值
}

geneticist.toUpperCase(); // 這會報錯,因為 geneticist 可能是 undefined。

執行真假值檢查在邏輯運算符也是可以運作的,例:

geneticist && geneticist.toUpperCase(); // OK, string | undefined
geneticist?.toUpperCase(); // OK, string | undefined

沒有初始值的變數

沒有初始值的變數宣告,在 JavaScript 中,預設為 undefined。

在型別系統中提出了一個情況:如果將變數宣告為不包含 undefined 的型別,然後在指派資料之前嚐試使用的話,就會報出錯誤訊息

let a: string;
a?.length; // 報錯:變數 a 在指派之前使用

a = "Hi";
a.length; // OK

但如果變數的型別包含 undefined 的話,那就不會報錯了,例:

let a: string | undefined;
a?.length; // OK

a = "Hi";
a.length; // OK

型別別名

以下程式為範例:

let rawDataFirst: boolean | number | string | null | undefined;
let rawDataSecond: boolean | number | string | null | undefined;
let rawDataThird: boolean | number | string | null | undefined;

TypeScript 允許型別別名(type aliases),變為更簡單的名稱。

型別別名以關鍵字 type、型別名稱、等號作為開頭,習慣上,型別別名以大寫駝峰式來寫,例:

type MyName = …;

上述程式,可改寫成:

type RawData = boolean | number | string | null | undefined;

let rawDataFirst: RawData;
let rawDataSecond: RawData;
let rawDataThird: RawData;

變得更易讀了。

JavaScript 沒有型別別名

也就是上述程式碼,若編譯為 JavaScript 時,會是如下:

let rawDataFirst;
let rawDataSecond;
let rawDataThird;

因為在 JavaScript 程式語法當中,是沒有型別別名這東西的,所以型別別名只會存在於 TypeScript 程式語法當中。

型別別名的組合

型別別名可以參考其它型別別名,例:

type Id = number | string;
type IdMaybe = Id | undefined | null; // 相當於 number | string | undefined | null

型別別名可以不用依照使用順序來宣告,所以也可以寫成如下:

type IdMaybe = Id | undefined | null;
type Id = number | string;

留言

發佈留言