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
註解:
let與const只要在宣告時有初始值,編譯器就能直接決定型別。之後若嘗試賦予不相容的型別,編譯器會報錯。
範例 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(若 noImplicitAny 為 false)或錯誤(若 noImplicitAny 為 true) |
| 陣列裡混合不同型別 | `Array<type1 |
使用 Object、Array、Function 等全域類別 |
object、any[]、(...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)變數的型別,使其在特定程式區段內變得更具體。常見的保護手段包括 typeof、instanceof、屬性檢查、自訂型別守衛 等。當 TypeScript 觀測到這些保護條件時,會 重新推論 變數的型別。
範例 5:typeof 與 instanceof 的型別縮窄
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)為true,pet就一定是Cat」。這讓後續的程式碼能安全地存取Cat的屬性或方法。
常見陷阱與最佳實踐
| 陷阱 | 可能的問題 | 解決方式 |
|---|---|---|
過度依賴 any |
失去型別安全,編譯器無法捕捉錯誤 | 盡量使用 unknown 取代 any,或加上自訂型別守衛 |
| 未啟用嚴格模式 | 隱式 any、隱式 undefined 等問題不會被偵測 |
在 tsconfig.json 中設定 "strict": true |
| 在函式參數上省略型別 | 呼叫端傳入錯誤型別,且 IDE 補完不完整 | 為公共 API、外部模組的函式 顯式標註 參數與回傳型別 |
| 陣列混雜不同型別 | 推論為聯集型別,使用時需頻繁型別檢查 | 使用 泛型 (Array<T>) 或 Tuple 來明確描述結構 |
| 物件字面量的「寬」推論」 | 只宣告部份屬性,導致後續使用時缺少屬性檢查 | 使用 as const 或 interface/type 來限定屬性集合 |
最佳實踐小結
- 在公共 API(函式、類別、模組)上使用顯式型別,讓使用者一目了然。
- 內部實作可適度依賴推論,減少冗長程式碼,同時保持可讀性。
- 開啟嚴格模式,尤其是
noImplicitAny、strictNullChecks,強制補上必要的型別。 - 善用型別保護:
typeof、instanceof、屬性檢查與自訂守衛,讓 TypeScript 能在分支內正確縮窄型別。 - 對於不確定的外部資料(如 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 應用!