本文 AI 產出,尚未審核

TypeScript – 型別推論與型別保護

顯式 vs 隱式型別


簡介

在 JavaScript 的世界裡,變數的型別是 動態彈性 的,這讓開發者在寫程式時可以快速驗證想法,但同時也埋下了 型別錯誤執行期例外 的隱憂。TypeScript 以靜態型別系統為基礎,提供 型別檢查自動補完重構安全 等好處,讓大型專案的維護成本大幅下降。

然而,TypeScript 並非要求你必須手寫每一個型別;它內建的 型別推論 (type inference) 會在大多數情況下自動推斷出最合適的型別。而在需要更精確控制或想要表達意圖時,我們則會使用 顯式型別 (explicit type annotation)。了解兩者的差異、何時使用哪一種,對於寫出 可讀可維護、且 安全 的程式碼至關重要。


核心概念

1. 什麼是隱式型別(型別推論)?

當變數、函式參數或回傳值沒有寫明型別,TypeScript 會根據 賦值表達式函式內部的返回語句控制流程 等資訊,自動推斷出一個最合適的型別。這個過程稱為 隱式型別型別推論

範例 1:基本變數推論

// 沒有寫明型別,TypeScript 會推論為 number
let count = 0;          // inferred as number

// 推論為 string
const greeting = "Hello, TypeScript!"; // inferred as string

註解letconst 只要在宣告時有初始值,編譯器就能直接決定型別。之後若嘗試賦予不相容的型別,編譯器會報錯。

範例 2:函式回傳值推論

function add(a: number, b: number) {
  return a + b; // 回傳值被推論為 number
}

const sum = add(3, 5); // sum 的型別是 number

註解:即使函式本身沒有寫回傳型別,因為 return 表達式是 number,編譯器會自動推論 add 的回傳型別為 number

2. 什麼是顯式型別(型別標註)?

顯式型別是指開發者主動在變數、函式參數、回傳值或物件屬性上寫出型別註記。這樣做的好處是 清晰表達意圖防止推論錯誤,以及在 API 文件 中提供更完整的說明。

範例 3:顯式參數與回傳型別

function formatPrice(price: number, currency: string = "USD"): string {
  // 明確宣告回傳型別為 string
  return `${currency} ${price.toFixed(2)}`;
}

const priceTag = formatPrice(1999.5, "TWD"); // priceTag 的型別是 string

註解:即使 currency 有預設值,我們仍寫出 string 型別,讓使用者一眼就能看出預期的資料型別。

3. 型別推論的限制與「最寬」推論

TypeScript 的推論遵循「最狹窄」原則(the most specific type),但在以下情況會退回「最寬」的型別:

情況 推論結果
變數宣告時沒有初始值 any(若 noImplicitAnyfalse)或錯誤(若 noImplicitAnytrue
陣列裡混合不同型別 `Array<type1
使用 ObjectArrayFunction 等全域類別 objectany[](...args: any[]) => any

範例 4:最寬推論導致的隱藏錯誤

let data;               // 沒有初始值,若 noImplicitAny 為 false,data 為 any
data = { name: "Alice" };
data = 123;             // 仍然可以賦值,編譯器不會報錯

// 正確做法:給予顯式型別
let user: { name: string };
user = { name: "Bob" };
// user = 123; // 編譯錯誤:Type 'number' is not assignable to type '{ name: string; }'

重點:在嚴格模式 (strict) 下,noImplicitAny 預設為 true,因此上述情況會直接產生錯誤,提醒開發者補上型別。

4. 型別保護(Narrowing)與推論的互動

型別保護是 縮窄(narrowing)變數的型別,使其在特定程式區段內變得更具體。常見的保護手段包括 typeofinstanceof、屬性檢查自訂型別守衛 等。當 TypeScript 觀測到這些保護條件時,會 重新推論 變數的型別。

範例 5:typeofinstanceof 的型別縮窄

function printId(id: number | string) {
  if (typeof id === "string") {
    // 在此區塊內,id 被縮窄為 string
    console.log(`字串 ID: ${id.toUpperCase()}`);
  } else {
    // 這裡則是 number
    console.log(`數字 ID: ${id.toFixed(0)}`);
  }
}

範例 6:自訂型別守衛(user-defined type guard)

interface Cat { meow(): void; }
interface Dog { bark(): void; }

function isCat(pet: Cat | Dog): pet is Cat {
  return (pet as Cat).meow !== undefined;
}

function interact(pet: Cat | Dog) {
  if (isCat(pet)) {
    // pet 被縮窄為 Cat
    pet.meow();
  } else {
    // pet 被縮窄為 Dog
    pet.bark();
  }
}

說明pet is Cat 的回傳型別告訴 TypeScript,「只要 isCat(pet)truepet 就一定是 Cat」。這讓後續的程式碼能安全地存取 Cat 的屬性或方法。


常見陷阱與最佳實踐

陷阱 可能的問題 解決方式
過度依賴 any 失去型別安全,編譯器無法捕捉錯誤 盡量使用 unknown 取代 any,或加上自訂型別守衛
未啟用嚴格模式 隱式 any、隱式 undefined 等問題不會被偵測 tsconfig.json 中設定 "strict": true
在函式參數上省略型別 呼叫端傳入錯誤型別,且 IDE 補完不完整 為公共 API、外部模組的函式 顯式標註 參數與回傳型別
陣列混雜不同型別 推論為聯集型別,使用時需頻繁型別檢查 使用 泛型 (Array<T>) 或 Tuple 來明確描述結構
物件字面量的「寬」推論」 只宣告部份屬性,導致後續使用時缺少屬性檢查 使用 as constinterface/type 來限定屬性集合

最佳實踐小結

  1. 在公共 API(函式、類別、模組)上使用顯式型別,讓使用者一目了然。
  2. 內部實作可適度依賴推論,減少冗長程式碼,同時保持可讀性。
  3. 開啟嚴格模式,尤其是 noImplicitAnystrictNullChecks,強制補上必要的型別。
  4. 善用型別保護typeofinstanceof、屬性檢查與自訂守衛,讓 TypeScript 能在分支內正確縮窄型別。
  5. 對於不確定的外部資料(如 API 回傳),先用 unknown 接收,再透過型別守衛或 zod/io-ts 等驗證函式轉換為具體型別。

實際應用場景

1. 前端表單驗證

type FormData = {
  username: string;
  age?: number; // optional
};

function submitForm(data: FormData) {
  // 透過型別保護確保 age 為 number 時才使用
  if (typeof data.age === "number") {
    console.log(`使用者年齡: ${data.age}`);
  } else {
    console.log("年齡未提供");
  }
}

// 使用時,IDE 會自動提示必填欄位
submitForm({ username: "alice" });

說明FormData 為顯式型別,讓表單結構固定;在處理可選屬性時,利用 typeof 進行縮窄,避免 undefined 產生的執行期錯誤。

2. 與第三方 API 互動

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`https://api.example.com/users/${id}`);
  const json = await res.json(); // 先當作 unknown 處理
  // 使用型別守衛驗證結構
  if (isUser(json)) {
    return json; // 已確定為 User
  }
  throw new Error("API 回傳資料不符合預期");
}

// 型別守衛
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    "id" in obj &&
    "name" in obj &&
    typeof (obj as any).id === "number" &&
    typeof (obj as any).name === "string"
  );
}

說明:外部資料的型別不可靠,先以 unknown 接收,再透過自訂守衛 isUser 確認結構,最後才返回具體的 User 型別。這樣的流程在 微服務Node.js 後端React 中都相當常見。

3. 產生動態 UI 元件

type ButtonProps = {
  label: string;
  onClick: () => void;
  disabled?: boolean;
};

const Button = ({ label, onClick, disabled = false }: ButtonProps) => (
  <button onClick={onClick} disabled={disabled}>
    {label}
  </button>
);

// 呼叫時,TS 會自動補全 disabled 為可選布林值
<Button label="送出" onClick={() => console.log("送出")} />;

說明:在 React + TypeScript 的環境裡,顯式的 ButtonProps 為元件提供完整的型別資訊,編譯器與 IDE 能即時提示錯誤與自動補全;同時,預設參數 disabled = false 讓開發者不必每次都寫入布林值。


總結

  • 顯式型別意圖的聲明,適用於 API、公共函式、外部資料等需要清晰契約的地方。
  • **隱式型別(型別推論)**則是 開發者的便利,在局部變數、簡單函式內部可省去冗長的註記,提升開發速度。
  • 型別保護(Narrowing)是 TypeScript 靈活且安全的核心機制,讓變數在特定條件下自動收斂為更精確的型別。
  • 開啟 嚴格模式、避免過度使用 any、善用自訂型別守衛,才能在 安全效率 之間取得最佳平衡。

掌握顯式與隱式型別的使用時機,並結合型別保護的技巧,你的 TypeScript 程式碼將會更 可讀易維護,同時在編譯階段捕捉到更多潛在錯誤,減少上線後的意外。祝你寫程式愉快,寫出更健壯的 TypeScript 應用!