本文 AI 產出,尚未審核

TypeScript

單元:測試與型別驗證

主題:類型推論驗證


簡介

TypeScript 中,型別推論(type inference)是讓開發者不必手動寫太多型別註記、同時仍能享受靜態檢查好處的核心機制。
然而,推論得到的型別不一定就是我們期望的結果,若不加以驗證,程式在執行階段仍可能出現意外的錯誤。

本篇文章將說明 如何在測試或開發流程中驗證 TypeScript 的類型推論,讓你在寫程式的同時,確保「編譯器」所推斷的型別與實際需求相符。文章適合剛接觸 TypeScript 的新手,也能提供中階開發者在大型專案中維持型別安全的實務技巧。


核心概念

1. 為什麼要驗證型別推論?

  • 避免隱蔽的型別錯誤:推論可能因為變數初始值或函式返回值的寫法而產生過寬或過窄的型別。
  • 提升程式碼可讀性:明確驗證後,團隊成員可以快速了解每個變數的真正意圖。
  • 配合測試框架:在 Jest、Vitest 等單元測試中加入型別檢查,可在 CI 階段即捕捉型別回歸。

2. as const 與字面量推論

預設情況下,字面量會被推論為最寬鬆的型別(例如 stringnumber),使用 as const 可以把它固定為 字面量型別,從而在後續驗證中得到更精確的結果。

const status = "success";          // 推論為 string
const status2 = "success" as const; // 推論為 "success"

3. typeof + satisfies 兩大驗證手法

  • typeof:取得變數的型別,配合 介面型別別名 進行比較。
  • satisfies(TS 4.9 起):在賦值時同時檢查符合某個型別,而不會改變推論結果。
type User = {
  id: number;
  name: string;
};

const rawUser = {
  id: 1,
  name: "Alice",
  extra: true, // 多餘屬性
} satisfies User; // ✅ 編譯通過,且 rawUser 仍保留完整結構

4. 使用 expectType 於測試中斷言型別

tsdexpect-type 等套件提供 型別斷言,可在測試檔案裡直接寫出「我期望這個變數的型別是 X」的語句,編譯失敗即代表推論不符。

// install: npm i -D expect-type
import { expectType } from "expect-type";

const nums = [1, 2, 3] as const;
expectType<readonly [1, 2, 3]>(nums); // ✅ 通過

5. 透過 PartialRequiredPick 等工具型別檢查結構變化

在函式參數或回傳值的型別推論需要「部分」或「必須」屬性時,可結合 工具型別 進行驗證。

type Config = {
  host: string;
  port: number;
  timeout?: number;
};

function createConfig(conf: Partial<Config>) {
  return { host: "localhost", port: 80, ...conf };
}

// 驗證回傳值仍符合 Config(即使 timeout 可能缺少)
const cfg = createConfig({ port: 3000 });
type _ = Expect<Equal<typeof cfg, Config>>; // 若使用 tsd,可寫成 expectType<Config>(cfg);

程式碼範例

範例 1:基本推論與 as const

// 沒有 as const,推論為 string[]
const colors = ["red", "green", "blue"];
// 推論為 readonly ["red", "green", "blue"]
const colorsLiteral = ["red", "green", "blue"] as const;

// 驗證
type Colors = typeof colors;          // string[]
type ColorsLiteral = typeof colorsLiteral; // readonly ["red","green","blue"]

範例 2:使用 satisfies 防止多餘屬性

type Product = {
  id: number;
  name: string;
  price: number;
};

const p = {
  id: 101,
  name: "Keyboard",
  price: 2999,
  discount: 0.1, // ❌ 多餘屬性
} satisfies Product; // 編譯錯誤:Object literal may only specify known properties

範例 3:在 Jest 中結合 expect-type

// __tests__/inference.test.ts
import { expectType } from "expect-type";

function sum(a: number, b: number) {
  return a + b;
}

test("sum 的回傳型別應該是 number", () => {
  const result = sum(2, 3);
  expectType<number>(result); // ✅ 若 result 被推論為其他型別,測試會失敗
});

範例 4:利用工具型別驗證 API 回傳結構

type ApiResponse<T> = {
  data: T;
  error?: string;
};

async function fetchUser(): Promise<ApiResponse<{ id: number; name: string }>> {
  const res = await fetch("/api/user");
  const json = await res.json();
  return json;
}

// 測試型別
type UserResponse = Awaited<ReturnType<typeof fetchUser>>["data"];
// 期望 UserResponse 必須符合 { id: number; name: string }
type _Check = Expect<Equal<UserResponse, { id: number; name: string }>>; // 使用 tsd

範例 5:條件型別與推論結合

type IsString<T> = T extends string ? true : false;

function identify<T>(value: T): IsString<T> {
  return (typeof value === "string") as IsString<T>;
}

// 使用
const a = identify("hello"); // 推論為 true
const b = identify(123);     // 推論為 false

// 驗證
type TestA = Expect<Equal<typeof a, true>>; // ✅
type TestB = Expect<Equal<typeof b, false>>; // ✅

常見陷阱與最佳實踐

陷阱 說明 解決方案
推論過寬:陣列或物件被推成 any[]object 可能因為缺少初始化值或使用 any 造成 使用 as const、明確的型別註記或 satisfies
多餘屬性不被偵測:直接賦值給介面時會被寬容 會導致不必要的資料流入系統 satisfiesExact 型別(自訂)
條件型別推論失敗:在泛型函式內部使用 typeof 取得錯誤結果 必須在函式外部或使用 infer 取得 盡量把推論邏輯抽成輔助型別
測試環境缺少型別斷言套件 只跑 JavaScript 測試,型別錯誤不會被捕捉 在 CI 中加入 tsdexpect-type 的型別測試階段
過度使用 any 失去 TypeScript 靜態檢查的意義 設定 noImplicitAnystrict 模式,必要時改用 unknown 再做型別縮小

最佳實踐

  1. 預設啟用 strict 模式,包括 noImplicitAnystrictNullChecks
  2. 在關鍵 API、公共函式上使用 satisfies,保證結構正確且不改變推論。
  3. 將型別斷言測試納入 CI,即使是小型專案也能提前發現型別回歸。
  4. 盡量利用 as const 鎖定字面量,避免因為寬鬆推論導致錯誤傳遞。
  5. 結合工具型別PartialRequiredPick)在函式簽名中明確描述「可選」與「必須」的屬性。

實際應用場景

  1. 前端 UI Component Library

    • 每個 component 的 props 會使用 satisfies 來驗證使用者傳入的屬性,確保不會因為多餘屬性造成渲染錯誤。
    • 在 Storybook 測試中加入 expect-type,自動檢查每個 story 的 props 型別。
  2. Node.js 後端 API

    • 透過 type ApiResponse<T> 結合 satisfies,保證所有回傳 JSON 符合統一規範。
    • tsd 在 CI 中驗證所有服務端函式的返回型別,避免前端呼叫時出現 undefined。
  3. 大型單元測試套件

    • 在 Jest/Vitest 測試檔案中加入 expect-type,讓測試同時檢查功能正確性與型別正確性。
    • 針對泛型函式(如資料庫查詢工具)寫 型別單元測試,確保不同資料模型下的推論皆符合預期。

總結

類型推論驗證 是把 編譯期的型別安全 進一步延伸到 開發與測試流程 的關鍵步驟。
透過 as constsatisfies、工具型別以及專門的型別斷言套件,我們可以在 不犧牲開發效率 的同時,確保程式碼的 型別正確性可維護性

在日常開發中,養成以下習慣即可大幅降低型別相關的 bug:

  1. 嚴格的 tsconfig 設定strictnoImplicitAny)。
  2. 對外部介面使用 satisfies,避免多餘屬性。
  3. 在單元測試中加入型別斷言,讓 CI 成為型別守門人。
  4. 適時使用 as const,固定字面量型別。

只要把型別驗證當作程式碼的一部分,而不是額外的「檢查」工作,你的 TypeScript 專案將會更安全、更易於擴充。祝你寫程式快樂,型別永遠正確!