本文 AI 產出,尚未審核
TypeScript 型別相容性與型別系統 – Type Widening / Narrowing
簡介
在 TypeScript 中,型別相容性(type compatibility)是靜態檢查的核心機制,而 type widening 與 type narrowing 則是「讓程式碼在型別安全與彈性之間取得平衡」的兩把關鍵鑰匙。
- Widening:在變數宣告或運算過程中,TypeScript 會把較窄的字面值型別自動擴大成更寬廣的型別(例如
1→number),讓程式碼不必每次都寫明確的型別。 - Narrowing:相反的,當程式執行到分支判斷或類型守護(type guard)時,TypeScript 會把寬泛的型別「縮小」成更具體的型別,讓開發者可以安全地存取屬性或呼叫方法。
掌握這兩個概念,不僅能減少冗長的型別宣告,還能避免因型別過寬或過窄而產生的執行時錯誤。以下將以實作範例說明其運作原理與最佳使用方式。
核心概念
1. 什麼是 Type Widening?
當 TypeScript 推斷一個變數的型別時,若它的值是字面量(literal),預設會把它 「寬化」 成對應的基本型別。
let a = 10; // a 被推斷為 number(而非 10)
let b = "hello"; // b 被推斷為 string(而非 "hello")
const c = true; // const 會保留字面值型別,c 為 true
let/var:會自動寬化,因為變數之後可能被重新指派不同的值。const:保留字面值型別,除非使用as const進一步凍結。
為何需要 Widening?
- 減少重複型別註記:不必每次都寫
number、string,編譯器會幫你推斷。 - 提升彈性:允許在後續的程式流程中改變值的型別(只要相容)。
2. 什麼是 Type Narrowing?
在程式的控制流程(if、switch、instanceof、typeof 等)裡,TypeScript 會根據條件把變數的型別 「縮小」,讓開發者可以安全使用更具體的屬性或方法。
function printLength(x: string | number) {
if (typeof x === "string") {
// 在此分支中,x 被縮小為 string
console.log(x.length);
} else {
// 這裡的 x 為 number
console.log(x.toFixed(2));
}
}
常見的縮小技巧
| 判斷方式 | 會縮小的型別 |
|---|---|
typeof |
string、number、boolean、symbol、bigint、function |
instanceof |
任何 class 或 constructor |
in (property existence) |
具備該屬性的物件型別 |
自訂型別守護 (x is Y) |
任意使用者自訂的型別 |
3. Widening 與 Narrowing 的互動
以下範例示範在同一段程式中,先被寬化再被縮小的完整流程。
let value = "TS"; // value 被寬化為 string
function process(v: string | number) {
if (typeof v === "string") {
// 進入此分支,v 被縮小回字面值型別 string
console.log(`字串長度 ${v.length}`);
} else {
console.log(`數字 ${v.toFixed(1)}`);
}
}
process(value); // 正常呼叫,編譯器知道 value 為 string
如果把 value 改成 const value = "TS" as const;,則在 process 呼叫時,value 的型別會是字面值 "TS",仍然符合 string 的相容性,但在更嚴格的函式簽名中(例如 function foo(v: "TS"))就能直接匹配。
4. 具體範例
範例 1:使用 as const 防止過度寬化
const colors = ["red", "green", "blue"]; // 推斷為 string[]
// 加上 as const,變成 readonly ["red","green","blue"]
const colorsLiteral = ["red", "green", "blue"] as const;
// 只接受字面值型別的函式
function setTheme(color: "red" | "green" | "blue") {
console.log(`主題顏色:${color}`);
}
setTheme(colors[0]); // ❌ 編譯錯誤,因為 colors[0] 為 string
setTheme(colorsLiteral[0]); // ✅ 正確,因為 colorsLiteral[0] 為 "red"
重點:
as const可以「凍結」陣列或物件,使其保持最窄的字面值型別,避免不必要的寬化。
範例 2:利用 instanceof 縮小類別型別
class Dog {
bark() { console.log("汪汪"); }
}
class Cat {
meow() { console.log("喵喵"); }
}
type Pet = Dog | Cat;
function speak(pet: Pet) {
if (pet instanceof Dog) {
// pet 被縮小為 Dog
pet.bark();
} else {
// pet 為 Cat
pet.meow();
}
}
範例 3:自訂型別守護(type guard)
interface Square {
kind: "square";
size: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Circle;
// 型別守護函式
function isSquare(s: Shape): s is Square {
return s.kind === "square";
}
function area(shape: Shape) {
if (isSquare(shape)) {
// shape 被縮小為 Square
return shape.size ** 2;
}
// 此時 shape 為 Circle
return Math.PI * shape.radius ** 2;
}
範例 4:in 操作符縮小物件型別
type Admin = { role: "admin"; access: number };
type Guest = { role: "guest"; visitCount: number };
type User = Admin | Guest;
function describe(u: User) {
if ("access" in u) {
// u 為 Admin
console.log(`管理員權限等級 ${u.access}`);
} else {
// u 為 Guest
console.log(`訪客已來訪 ${u.visitCount} 次`);
}
}
範例 5:從寬化到縮小的實務流程
let input = document.getElementById("num") as HTMLInputElement; // 型別被寬化為 HTMLElement
if (input instanceof HTMLInputElement) {
// 縮小為 HTMLInputElement,安全取得 value
const value = Number(input.value);
console.log(`輸入的數字是 ${value}`);
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 過度寬化導致失去字面值資訊 | 使用 let 或 var 時,字面值會自動變成寬泛型別,導致後續無法利用字面值的安全性。 |
需要保留字面值時,改用 const 或 as const。 |
| 縮小後仍被視為寬泛型別 | 某些條件(如 typeof x === "object")只能縮小到 object,無法辨識更具體的介面。 |
搭配 in、自訂型別守護或 instanceof 提供更精細的縮小。 |
使用 any 破壞縮小機制 |
若變數被宣告為 any,TypeScript 會放棄所有型別檢查,縮小也失效。 |
盡量避免 any,改用 unknown 並在需要時手動縮小。 |
| 函式重載與寬化衝突 | 重載簽名過於寬泛會讓編譯器無法正確推斷返回型別。 | 為每個重載提供具體的參數型別,並在實作中使用縮小。 |
| 陣列/物件的可變性 | 直接把 let arr = [] 推斷為 any[],容易產生錯誤。 |
指定初始型別或使用 as const 讓陣列保持只讀。 |
最佳實踐
- 先寬化,再縮小:在變數宣告時允許寬化,提高彈性;在使用時透過型別守護縮小,確保安全。
- 盡量使用
const:保留字面值型別,減少不必要的寬化。 - 利用
as const:對不可變的資料結構(如設定檔、列舉)凍結型別。 - 寫自訂型別守護:對於複雜聯合型別,提供明確的
isX函式,讓編譯器能正確縮小。 - 避免
any:若必須使用,改用unknown,配合縮小後再斷言。
實際應用場景
- 表單驗證
- 使用
HTMLInputElement的寬化 (HTMLElement) 取得元素,透過instanceof縮小後安全存取value。
- 使用
- API 回傳資料的型別守護
- 從後端取得的 JSON 可能是多種形態,使用
in或自訂型別守護判斷屬性是否存在,縮小為正確的介面再進行業務邏輯。
- 從後端取得的 JSON 可能是多種形態,使用
- Redux / Zustand 狀態管理
- 狀態常以聯合型別表示(
Loading | Success<T> | Failure),在 UI 渲染時透過switch或if判斷status,縮小型別後直接使用對應屬性。
- 狀態常以聯合型別表示(
- 函式重載與多態
- 例如
function format(input: string): string; function format(input: number): string;,在實作中先寬化為string | number,再用typeof縮小分別處理。
- 例如
- 設定檔與常數列舉
- 使用
as const定義不可變的設定物件,讓後續的函式只能接受列舉中明確的字面值,提升編譯期安全性。
- 使用
總結
- Type Widening 讓變數在宣告時自動擴展為寬泛型別,減少冗餘的型別標註;
- Type Narrowing 則在程式的控制流程中把寬泛型別縮小為具體型別,使得屬性存取與方法呼叫更安全。
- 兩者配合使用,是 TypeScript 型別系統中最實用的技巧之一,能在保持彈性的同時,提供強大的編譯期檢查。
掌握 寬化 → 縮小 的思考模式,並善用 const、as const、instanceof、typeof、in 以及自訂型別守護,你就能寫出 既安全又易維護 的 TypeScript 程式碼。祝你在日常開發中玩得開心、寫得更好!