TypeScript 基本型別 – any 與 unknown 的差異與實務運用
簡介
在 TypeScript 中,型別安全是最核心的概念之一。它能在編譯階段捕捉錯誤,減少執行時意外。
然而,當我們與外部資料(例如 API 回傳、第三方函式庫或動態生成的 JSON)互動時,往往無法事先知道確切的型別。這時就會用到兩個特殊的原始型別:any 與 unknown。
any 與 unknown 看起來很相似,都表示「不知道是什麼型別」,但行為上卻大相逕庭。了解它們的差異,能讓你在保持彈性的同時,仍然保有 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 個實用範例,說明 any 與 unknown 在不同情境下的使用方式與差異。
範例 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強迫開發者在使用資料前先檢查,避免undefined或null引發的執行時錯誤。
範例 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:any 與 unknown 在函式參數的差異
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 const 與 unknown,或在 .d.ts 中明確聲明型別 |
最佳實踐:
- 預設使用
unknown:當必須接受不確定的資料時,先宣告為unknown。 - 僅在必要時使用
any:例如快速原型或第三方庫缺少型別宣告。 - 搭配型別保護:寫 reusable 的 type guard,讓
unknown轉換為具體型別更簡潔。 - 啟用
noImplicitAny、strict:在tsconfig.json中開啟嚴格模式,防止不小心產生隱式any。
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true
}
}
實際應用場景
API 回傳的 JSON
- 伺服器端可能因版本升級或錯誤回傳不同結構。使用
unknown接收,並在服務層做 資料驗證(如zod、io-ts)後再轉型。
- 伺服器端可能因版本升級或錯誤回傳不同結構。使用
第三方插件或舊有 JavaScript 程式庫
- 這類庫往往沒有型別檔,直接把返回值視為
any會讓程式碼缺乏安全性。先宣告為unknown,再在 wrapper 函式中做型別縮減。
- 這類庫往往沒有型別檔,直接把返回值視為
動態生成的 UI 元件
- 例如根據使用者自行上傳的設定檔決定表單欄位。設定檔內容先以
unknown讀入,確定每個欄位的型別後再渲染。
- 例如根據使用者自行上傳的設定檔決定表單欄位。設定檔內容先以
測試環境的 Mock Data
- 為了快速寫測試,可暫時使用
any,但在測試完成後建議改為unknown,讓測試本身也受益於型別檢查。
- 為了快速寫測試,可暫時使用
Node.js 中的
process.envprocess.env的每個屬性都是字串或undefined,若直接使用any會失去提示。可把它視為unknown,再透過自訂的型別保護轉成string | undefined。
總結
any放棄型別檢查,使用時要非常謹慎;適合暫時性或快速原型。unknown保留型別安全,必須在使用前先做型別縮減,是處理外部、不確定資料的首選。- 透過 型別保護、嚴格編譯選項 與 適當的斷言,可以在保持彈性的同時,最大化 TypeScript 的錯誤捕捉能力。
掌握 any 與 unknown 的差異與正確使用時機,能讓你的 TypeScript 程式碼既靈活又可靠,在面對多變的前端或後端需求時,仍能維持高品質的開發體驗。祝你寫程式愉快,型別安全永相伴!