TypeScript 變數與常數宣告:let / const 的型別推論
簡介
在 JavaScript 裡,var、let、const 已經是我們日常使用的變數宣告關鍵字。轉向 TypeScript 時,最常被問到的問題,就是 「TypeScript 會自動推論變數的型別嗎?」。答案是肯定的,而且推論的結果往往足以滿足大多數開發需求,讓我們可以在不寫冗長型別註記的情況下,仍然享受到靜態型別檢查的好處。
掌握 let / const 的型別推論,不僅能提升開發效率,還能減少因型別不一致而產生的 bug。特別是 const,它的推論行為與 let 有所不同,了解兩者的差異是寫出安全、可維護程式碼的關鍵。
核心概念
1. 基本推論規則
當我們以 let 或 const 宣告變數,且 沒有手動指定型別 時,TypeScript 會根據右側的初始值自動推斷型別。
| 宣告方式 | 初始值 | 推論結果 |
|---|---|---|
let a = 10; |
整數 | number |
const b = "Hello"; |
字串 | 字面量型別 \"Hello\" |
let c = true; |
布林值 | boolean |
const d = [1, 2, 3]; |
陣列 | 只讀陣列 readonly [1, 2, 3](實際上是 number[],但若使用 as const 會變成只讀) |
let e = { x: 1, y: "a" }; |
物件 | { x: number; y: string; } |
重點:
const在推論時會盡可能保留字面量型別(literal type),而let則會把字面量提升為一般型別(例如number、string)。
2. let 與 const 的差異
| 觀點 | let |
const |
|---|---|---|
| 可重新指派 | ✅ 可以let x = 1; x = 2; |
❌ 不可const y = 1; // y = 2; // 錯誤 |
| 型別推論 | 只保留一般型別let a = 5; // a: number |
保留字面量型別const b = 5; // b: 5 |
| 適用情境 | 需要在之後變更值的情況 | 值在生命週期內不會變動,或希望 凍結 型別以避免誤改 |
為什麼 const 會保留字面量型別?
const MODE = "development";
// 推論結果:MODE: "development"(字面量型別)
let mode = "development";
// 推論結果:mode: string(一般字串型別)
在上例中,若我們把 MODE 傳給只能接受 "development" 或 "production" 其中之一的函式,TypeScript 能直接檢查通過;而 mode 則會被視為一般 string,必須額外斷言或限制。
3. 使用 as const 強化推論
有時候我們希望 let 或 物件/陣列 也能得到更嚴格的字面量型別,這時可以使用 as const 斷言:
let config = {
host: "localhost",
port: 3000,
debug: true,
} as const;
// 推論結果:
// config: {
// readonly host: "localhost";
// readonly port: 3000;
// readonly debug: true;
// }
as const 會把所有屬性都轉成 只讀,同時保留字面量型別,讓後續的型別檢查更精確。
4. 推論的範圍與限制
- 函式參數:若未指定參數型別,預設為
any,不會自動根據傳入值推論。 - 變數未初始化:
let x;會被推論為any,因為缺乏足夠資訊。 - 聯合型別:若初始化值是多種型別的混合,TypeScript 會產生 聯合型別(union type)。
let mixed = Math.random() > 0.5 ? 42 : "answer";
// 推論結果:mixed: number | string
程式碼範例
以下提供 5 個實用範例,說明 let / const 在不同情境下的型別推論行為與最佳寫法。
範例 1:簡單數值與字串
let counter = 0; // 推論為 number
const greeting = "Hi!"; // 推論為 "Hi!"(字面量型別)
// 嘗試重新指派
counter = 10; // ✅ 正常
// greeting = "Hello"; // ❌ 編譯錯誤:Cannot assign to 'greeting' because it is a constant.
說明:
counter可隨時變動,型別保持為number;greeting雖然是字串,但因使用const,型別被鎖定為特定字面量"Hi!",避免不小心改成其他字串。
範例 2:陣列的推論與 as const
let numbers = [1, 2, 3]; // 推論為 number[]
const colors = ["red", "green"] as const; // 推論為 readonly ["red", "green"]
// 變更陣列元素
numbers.push(4); // ✅ 正常
// colors.push("blue"); // ❌ 編譯錯誤:Property 'push' does not exist on type 'readonly ["red", "green"]'.
說明:
numbers是可變的number[],而colors使用as const變成只讀陣列,防止在執行階段意外修改。
範例 3:物件的型別推論與凍結
let user = {
id: 123,
name: "Alice",
}; // 推論為 { id: number; name: string; }
const settings = {
theme: "dark",
version: 2,
} as const; // 推論為 { readonly theme: "dark"; readonly version: 2; }
// 變更屬性
user.name = "Bob"; // ✅ 正常
// settings.theme = "light"; // ❌ 編譯錯誤:Cannot assign to 'theme' because it is a read-only property.
說明:
user的屬性可以自由變更,適合需要更新的資料模型;settings則使用as const讓所有屬性變成唯讀,適合作為全域設定或常量。
範例 4:函式回傳值的型別推論
function createPoint(x: number, y: number) {
return { x, y }; // 推論為 { x: number; y: number; }
}
const origin = createPoint(0, 0); // origin: { x: number; y: number; }
說明:即使函式本身沒有明確指定回傳型別,TypeScript 仍會根據返回的物件自動推論出正確的結構。若希望回傳只讀的點,可加上
as const:
function createReadonlyPoint(x: number, y: number) {
return { x, y } as const; // 推論為 { readonly x: number; readonly y: number; }
}
範例 5:聯合型別與條件推論
let flag = Math.random() > 0.5 ? true : false; // 推論為 boolean
let result = Math.random() > 0.5 ? 100 : "hundred";
// 推論為 number | string
// 使用 type guard 取得更精確的型別
if (typeof result === "number") {
// 此區塊內 result 被視為 number
console.log(result.toFixed(2));
} else {
// 此區塊內 result 被視為 string
console.log(result.toUpperCase());
}
說明:
result的型別是聯合型別,透過typeof檢查,我們可以在不同分支取得正確的型別資訊,避免不必要的類型斷言。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
未初始化的 let 變數會變成 any |
let x; 沒有提供足夠資訊,TS 只能推論為 any,失去型別安全。 |
立即賦值或明確指定型別:let x: number; |
const 推論過於嚴格導致無法重用 |
把整個陣列或物件 as const 後,所有屬性皆變只讀,若需要修改會出錯。 |
僅在真正不會變動的情況下使用 as const,或使用 readonly 介面定義部分屬性為只讀。 |
混用 let/const 造成型別不一致 |
例如 const a = 1; let b = a;,b 會被推論為 number(失去字面量),可能在比較時出現意外。 |
若需要保持字面量型別,**同樣使用 const**或在 b 上使用 as const:let b = a as const; |
| 函式參數缺乏型別 | 直接把參數傳入未指定型別的函式會得到 any,削弱型別檢查。 |
為函式參數明確註記型別或使用 泛型。 |
使用 any 逃避推論 |
把變數宣告為 any 會失去 TypeScript 的所有好處。 |
盡量避免 any,改用 unknown 或具體型別。 |
最佳實踐
- 預設使用
const:除非確定需要重新指派,否則使用const讓編譯器自動保留字面量型別。 - 適度使用
as const:在需要「凍結」資料(如設定檔、列舉值)時使用,避免在普通變數上濫用。 - 初始化即賦值:讓 TypeScript 能在宣告時就完成推論,提升型別安全。
- 結合
readonly介面:對於大型物件,只想凍結部分屬性時,使用介面或型別別名定義readonly屬性。 - 利用型別守衛(type guard)處理聯合型別,減少
as斷言的使用。
實際應用場景
| 場景 | 為什麼推論重要 | 範例 |
|---|---|---|
| 環境變數設定 | 環境名稱往往是固定字面量("development"、"production"),使用 const 推論可避免拼寫錯誤。 |
```javascript const ENV = process.env.NODE_ENV as "development" |
| API 回傳資料型別 | 從後端取得的 JSON 可能結構固定,利用 as const 定義「只讀」樣板,讓編譯器在編寫取值程式時給予提示。 |
javascript const USER_SAMPLE = { id: 0, name: "", role: "admin" } as const; type User = typeof USER_SAMPLE; // { readonly id: 0; readonly name: ""; readonly role: "admin"; } |
| Redux / Zustand 狀態管理 | 狀態值多為不可變(immutable),使用 const + as const 能確保 reducer 不會意外改變原始值。 |
javascript const initialState = { count: 0, status: "idle" } as const; type State = typeof initialState; // readonly count: 0; readonly status: "idle"; |
| React Hook 的依賴陣列 | useEffect 的依賴陣列若使用字面量常數,TS 能自動檢查是否遺漏或多餘。 |
javascript const API_URL = "https://api.example.com" as const; useEffect(() => { fetch(API_URL).then(...); }, [API_URL]); // 若改為 let API_URL = ...,TS 仍能推論,但 const 更安全。 |
| 列舉 (Enum) 替代方案 | 透過 as const 的物件可模擬字串列舉,同時保留字面量型別。 |
```javascript const Colors = { Red: "red", Green: "green", Blue: "blue" } as const; type Color = keyof typeof Colors; // "Red" |
總結
let與const皆支援 型別推論,但const會保留更精確的 字面量型別,讓程式在編譯階段就能捕捉到不符合預期的值。- 使用
as const可以把let、物件、陣列 也提升為只讀且保留字面量型別的形式,適合「凍結」不變資料。 - 了解推論的限制(未初始化、
any、聯合型別)以及常見陷阱,能讓我們寫出更安全、可維護的程式碼。 - 在實務開發中,預設使用
const、適度使用as const、及時賦值,是提升 TypeScript 效益的最佳策略。
掌握了 let / const 的型別推論之後,你將能在 減少冗餘型別註記 的同時,仍保有 強大的靜態檢查,讓前端專案更穩定、更易於維護。祝你在 TypeScript 的旅程中寫出更乾淨、更安全的程式碼!