本文 AI 產出,尚未審核

TypeScript 基本型別 – anyunknown 的差異與實務運用


簡介

在 TypeScript 中,型別安全是最核心的概念之一。它能在編譯階段捕捉錯誤,減少執行時意外。
然而,當我們與外部資料(例如 API 回傳、第三方函式庫或動態生成的 JSON)互動時,往往無法事先知道確切的型別。這時就會用到兩個特殊的原始型別:anyunknown

anyunknown 看起來很相似,都表示「不知道是什麼型別」,但行為上卻大相逕庭。了解它們的差異,能讓你在保持彈性的同時,仍然保有 TypeScript 的型別檢查優勢,寫出更可靠的程式碼。


核心概念

1. any:放棄型別檢查

any 等同於「關掉」TypeScript 的型別系統。對於被標註為 any 的變數,編譯器會 允許任何操作,不會給出警告或錯誤。

let data: any = "hello";
data = 123;               // ✅ 不會報錯
data.trim();              // ✅ 即使此時 data 可能是 number,也不會提示
data.foo.bar();           // ✅ 完全不檢查屬性是否存在

為什麼會有 any

  • 快速遷移舊有 JavaScript 專案
  • 暫時性繞過型別不明的情況
  • 第三方庫沒有提供型別宣告

⚠️ 一旦大量使用 any,型別安全的好處就會消失,等於回到純 JavaScript。


2. unknown:安全的「未知」

unknown 也是「不知道是什麼型別」的意思,但它 保留了型別檢查。對 unknown 變數執行任何操作前,都必須先 做型別縮減(type narrowing),否則編譯器會報錯。

let payload: unknown = fetchData();   // 假設 fetchData 回傳 unknown

payload = "test";                     // 允許重新指派
payload.toUpperCase();                // ❌ 編譯錯誤:Object is of type 'unknown'.

if (typeof payload === "string") {
  // 在此區塊內,payload 被縮減為 string
  console.log(payload.toUpperCase()); // ✅ 正常
}

unknown 的特性

特性 any unknown
直接存取屬性/方法 ✅ (不檢查) ❌ (必須先縮減)
直接賦值給其他型別 ✅ (隱式轉型) ❌ (需要斷言或縮減)
賦值給 any
賦值給具體型別 ✅ (危險) ❌ (安全)

3. 何時使用 any、何時使用 unknown

情境 建議使用
需要快速寫原型、暫時不確定型別、且不會在產品上長期保留 any
接收外部 JSON、動態資料、或第三方 API,且希望在使用前檢查型別 unknown
想保留型別推斷,同時避免隱式 any 警告 unknown

程式碼範例

以下示範 5 個實用範例,說明 anyunknown 在不同情境下的使用方式與差異。

範例 1:從 fetch 取得不確定資料

async function getUser(): Promise<unknown> {
  const res = await fetch("/api/user");
  return res.json();               // json() 回傳 any,這裡改寫成 unknown
}

// 使用前先做型別縮減
async function showUserName() {
  const data = await getUser();

  // ✅ 正確的型別縮減
  if (typeof data === "object" && data !== null && "name" in data) {
    const name = (data as { name: string }).name;
    console.log("使用者名稱:", name);
  } else {
    console.warn("取得的資料格式不正確");
  }
}

重點:使用 unknown 強迫開發者在使用資料前先檢查,避免 undefinednull 引發的執行時錯誤。


範例 2:第三方庫的 any 回傳值,轉為 unknown

declare const legacyLib: {
  // 這是一個舊有的 JavaScript 函式,回傳 any
  getConfig: () => any;
};

const rawConfig: unknown = legacyLib.getConfig();

// 透過型別斷言或縮減取得安全的型別
if (typeof rawConfig === "object" && rawConfig !== null) {
  const config = rawConfig as { env: string; debug: boolean };
  console.log(`環境: ${config.env}, Debug: ${config.debug}`);
}

範例 3:使用 any 簡化測試時的 Mock

// 在單元測試中,我們常會 mock 一個函式的回傳值
function fetchData(): any {
  // 這裡故意回傳 any,讓測試可以自行決定型別
  return Math.random() > 0.5 ? "string data" : 42;
}

// 測試程式
const result = fetchData();
if (typeof result === "string") {
  console.log(result.toUpperCase()); // ✅ 正常
} else {
  console.log(result.toFixed(2));    // ✅ 正常
}

提醒:在測試環境使用 any 可快速建立 Mock,但務必在正式程式碼中避免。


範例 4:unknown 與型別保護(type guard)

function isPerson(obj: unknown): obj is { name: string; age: number } {
  return (
    typeof obj === "object" &&
    obj !== null &&
    "name" in obj &&
    typeof (obj as any).name === "string" &&
    "age" in obj &&
    typeof (obj as any).age === "number"
  );
}

const input: unknown = JSON.parse('{"name":"Alice","age":30}');

if (isPerson(input)) {
  // 在此區塊內,TypeScript 已知 input 為 { name: string; age: number }
  console.log(`${input.name} (${input.age} 歲)`);
}

範例 5:anyunknown 在函式參數的差異

function logValueAny(value: any) {
  // 直接使用,不會有任何檢查
  console.log(value.toString());
}

function logValueUnknown(value: unknown) {
  // 必須先做縮減
  if (typeof value === "object" && value !== null) {
    console.log(JSON.stringify(value));
  } else {
    console.log(String(value));
  }
}

// 呼叫
logValueAny(123);            // ✅ 正常
logValueAny(undefined);      // ✅ 正常(會拋出 runtime error,編譯不會警告)

logValueUnknown(123);       // ✅ 正常
logValueUnknown(undefined); // ✅ 正常

結論unknown 讓函式的使用者必須先確認型別,降低錯誤風險;any 則把責任全交給呼叫端。


常見陷阱與最佳實踐

陷阱 可能的後果 解決方案 / 最佳實踐
過度使用 any 失去型別安全,執行時錯誤難以追蹤 盡量改用 unknown,或在必要時使用 as 斷言
unknown 直接賦值給具體型別 編譯錯誤,或因斷言錯誤導致 runtime 錯誤 使用 型別保護(type guard)或 typeof/instanceof 檢查
忽略 null / undefined 的檢查 Object is possibly 'null' 錯誤或程式崩潰 在縮減時加入 !== null 判斷
在函式返回值上使用 any 使用者無法得知正確型別,容易傳錯參數 若返回值不確定,改為 unknown,並在文件說明需要自行檢查
在全域變數上使用 any 影響整個專案的型別推斷 使用 declare constunknown,或在 .d.ts 中明確聲明型別

最佳實踐

  1. 預設使用 unknown:當必須接受不確定的資料時,先宣告為 unknown
  2. 僅在必要時使用 any:例如快速原型或第三方庫缺少型別宣告。
  3. 搭配型別保護:寫 reusable 的 type guard,讓 unknown 轉換為具體型別更簡潔。
  4. 啟用 noImplicitAnystrict:在 tsconfig.json 中開啟嚴格模式,防止不小心產生隱式 any
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "noImplicitThis": true
  }
}

實際應用場景

  1. API 回傳的 JSON

    • 伺服器端可能因版本升級或錯誤回傳不同結構。使用 unknown 接收,並在服務層做 資料驗證(如 zodio-ts)後再轉型。
  2. 第三方插件或舊有 JavaScript 程式庫

    • 這類庫往往沒有型別檔,直接把返回值視為 any 會讓程式碼缺乏安全性。先宣告為 unknown,再在 wrapper 函式中做型別縮減。
  3. 動態生成的 UI 元件

    • 例如根據使用者自行上傳的設定檔決定表單欄位。設定檔內容先以 unknown 讀入,確定每個欄位的型別後再渲染。
  4. 測試環境的 Mock Data

    • 為了快速寫測試,可暫時使用 any,但在測試完成後建議改為 unknown,讓測試本身也受益於型別檢查。
  5. Node.js 中的 process.env

    • process.env 的每個屬性都是字串或 undefined,若直接使用 any 會失去提示。可把它視為 unknown,再透過自訂的型別保護轉成 string | undefined

總結

  • any 放棄型別檢查,使用時要非常謹慎;適合暫時性或快速原型。
  • unknown 保留型別安全,必須在使用前先做型別縮減,是處理外部、不確定資料的首選。
  • 透過 型別保護嚴格編譯選項適當的斷言,可以在保持彈性的同時,最大化 TypeScript 的錯誤捕捉能力。

掌握 anyunknown 的差異與正確使用時機,能讓你的 TypeScript 程式碼既靈活可靠,在面對多變的前端或後端需求時,仍能維持高品質的開發體驗。祝你寫程式愉快,型別安全永相伴!