本文 AI 產出,尚未審核

TypeScript 變數與常數宣告 – Narrowing(型別縮小)

簡介

在 TypeScript 中,型別縮小(Narrowing) 是編譯器根據程式的控制流程,將一個較寬鬆的聯合型別(union type)或 any 型別「縮」成更具體的型別。這項機制讓開發者在寫程式時可以保有彈性,同時在編譯階段獲得更精確的型別檢查與自動完成。

對於 變數與常數的宣告,正確運用型別縮小不僅能避免執行時的錯誤,還能提升程式碼的可讀性與維護性。本文將從概念說明、實作範例、常見陷阱到實務應用,帶你一步步掌握 TypeScript 的型別縮小技巧。


核心概念

1. 為什麼需要型別縮小?

  • 靜態安全:在執行前就能捕捉到不合法的屬性存取或方法呼叫。
  • 開發體驗:IDE 能根據縮小後的型別提供正確的提示與自動完成。
  • 程式可讀:透過條件分支明確表達「此時變數一定是 X 型別」的意圖。

2. 基本的縮小方式

條件 會觸發的縮小 範例
typeof 判斷 基本原始型別(stringnumberbooleansymbolbigintundefined if (typeof x === "string") { … }
instanceof 判斷 類別或建構子 if (obj instanceof Date) { … }
in 判斷 物件屬性存在性 if ("length" in value) { … }
相等比較 (===!==) 具體值的縮小 if (status === "success") { … }
真值判斷 (if (value)) 排除 nullundefined0""false if (user) { … }
判斷陣列 (Array.isArray) 確認是陣列 if (Array.isArray(data)) { … }
自訂型別守護 (value is Type) 任意複雜條件 function isPerson(v: any): v is Person { … }

下面分別說明每種情況,並提供實作範例。


3. typeof 型別守護

typeof 只能辨識 JavaScript 的原始型別,對於 objectfunction 只能得到 "object""function",因此常與其他守護結合使用。

function logValue(value: string | number) {
  if (typeof value === "string") {
    // 這裡 value 已被縮小為 string
    console.log(`字串長度:${value.length}`);
  } else {
    // 這裡 value 為 number
    console.log(`數值加 10:${value + 10}`);
  }
}

技巧typeof 比較時請使用字串常數(如 "string"),避免寫成變數造成編譯錯誤。


4. instanceof 判斷類別

instanceof 能辨識由 class 或建構子函式產生的實例。

class Cat {
  meow() { console.log("喵~"); }
}
class Dog {
  bark() { console.log("汪~"); }
}

function speak(animal: Cat | Dog) {
  if (animal instanceof Cat) {
    // animal 被縮小為 Cat
    animal.meow();
  } else {
    // animal 為 Dog
    animal.bark();
  }
}

注意:跨 iframe 或不同執行環境時,instanceof 可能失效,這時可改用 Object.prototype.toString.call 或自訂守護函式。


5. in 操作符檢查屬性

in 可用於判斷物件是否擁有特定屬性,常與 discriminated union(辨別聯合型別)搭配。

type Square = { kind: "square"; size: number };
type Circle = { kind: "circle"; radius: number };
type Shape = Square | Circle;

function area(shape: Shape) {
  if ("size" in shape) {
    // shape 為 Square
    return shape.size ** 2;
  } else {
    // shape 為 Circle
    return Math.PI * shape.radius ** 2;
  }
}

小提醒in 只能檢查 可列舉(enumerable)的屬性,對於 Symbol 或不可列舉屬性無效。


6. 相等比較的縮小

直接比較具體字面值或 enum 成員時,TypeScript 會根據比較結果縮小型別。

enum Status { Success = "success", Fail = "fail" }

function handle(status: Status) {
  if (status === Status.Success) {
    // status 為 Status.Success
    console.log("處理成功");
  } else {
    // status 為 Status.Fail
    console.log("處理失敗");
  }
}

7. 真值判斷(Truthiness)

if (value)&&|| 等布林運算中,TypeScript 會排除 falsynullundefined0NaN""false)的可能性。

function greet(name: string | null | undefined) {
  if (name) {
    // name 被縮小為 string
    console.log(`哈囉,${name}!`);
  } else {
    console.log("哈囉,陌生人!");
  }
}

陷阱:如果變數的型別同時包含 0"",真值判斷會把它們排除,可能不是你想要的行為。此時可改用 name != null 只排除 null/undefined


8. 陣列與 Array.isArray

Array.isArray 是判斷值是否為陣列的唯一安全方法,縮小結果會變成 any[] 或原本的元素型別陣列。

function flatten(input: number[] | number[][]) {
  if (Array.isArray(input[0])) {
    // input 被縮小為 number[][]
    return input.flat();
  } else {
    // input 為 number[]
    return input;
  }
}

9. 自訂型別守護(User‑defined type guard)

當內建守護不足時,我們可以寫一個返回型別謂詞 (value is Type) 的函式,讓編譯器知道縮小的結果。

interface Person {
  name: string;
  age: number;
}
function isPerson(value: any): value is Person {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof value.name === "string" &&
    typeof value.age === "number"
  );
}

function printInfo(obj: Person | string) {
  if (isPerson(obj)) {
    // obj 被縮小為 Person
    console.log(`${obj.name} (${obj.age} 歲)`);
  } else {
    // obj 為 string
    console.log(`字串:${obj}`);
  }
}

關鍵:型別守護函式的回傳型別必須寫成 value is Person,而非單純的 boolean,否則縮小不會生效。


常見陷阱與最佳實踐

陷阱 說明 解決方式
使用 any 抹掉縮小 若變數被宣告為 any,所有縮小都失效。 盡量避免 any,改用 unknown 搭配型別守護。
nullundefined 同時排除 真值判斷會同時排除 0"" 等,造成意外的型別變化。 使用 value != nullvalue !== undefined 只排除需要的情況。
跨執行環境的 instanceof 於不同 window/iframe 時,instanceof 可能不相等。 改用 Object.prototype.toString.call(value) === "[object Type]" 或自訂守護。
in 判斷可列舉屬性 in 無法檢測 Symbol 或不可列舉屬性。 若需要檢測 Symbol,使用 Object.getOwnPropertySymbols
過度依賴 as 斷言 手動斷言會跳過編譯器的縮小檢查,易產生 runtime 錯誤。 盡量使用型別守護,而非 as

最佳實踐

  1. 先用內建守護typeofinstanceofin)再考慮自訂守護。
  2. 保持變數的最小聯合型別,不要一次把太多型別混在一起。
  3. 使用 unknown 取代 any,配合守護函式提升安全性。
  4. 寫測試:即使有型別縮小,仍建議撰寫單元測試驗證邏輯分支。

實際應用場景

  1. API 回傳資料的型別安全
    從後端取得的 JSON 可能是 UserErrorResponse,使用 discriminated union 搭配 inkind 屬性可安全地取出資料。

    type ApiResult = { kind: "user"; data: User } |
                     { kind: "error"; message: string };
    
    function handleResult(res: ApiResult) {
      if (res.kind === "user") {
        console.log(`使用者名稱:${res.data.name}`);
      } else {
        console.error(`錯誤:${res.message}`);
      }
    }
    
  2. 表單輸入的多型別驗證
    表單欄位可能接受字串或數字,利用 typeof 與自訂守護,保證後續處理時型別正確。

  3. React 元件的 Props
    某個元件接受 string | ReactNode 作為子元素,透過 typeof 判斷可以決定是否直接渲染或包裝。

    interface Props { content: string | React.ReactNode }
    const Card: React.FC<Props> = ({ content }) => (
      <div className="card">
        {typeof content === "string" ? <p>{content}</p> : content}
      </div>
    );
    

總結

  • 型別縮小 是 TypeScript 讓靜態型別與動態程式流程相互配合的核心機制。
  • 透過 typeofinstanceofin、相等比較、真值判斷 以及 自訂型別守護,我們可以在不同分支中得到更精確的型別資訊。
  • 正確使用縮小能提升 安全性開發體驗,並降低 runtime 錯誤的風險。
  • 在實務開發中,建議先利用內建守護,必要時再撰寫自訂守護,並配合 unknown測試最佳實踐,打造穩健且易維護的 TypeScript 程式碼。

掌握了型別縮小,你就能在 變數與常數宣告 的每一次選擇中,都保持型別的清晰與安全,寫出更可靠的前端或 Node.js 應用。祝你在 TypeScript 的世界裡玩得開心、寫得順手!