本文 AI 產出,尚未審核

TypeScript 變數與常數宣告:let / const 的型別推論


簡介

在 JavaScript 裡,varletconst 已經是我們日常使用的變數宣告關鍵字。轉向 TypeScript 時,最常被問到的問題,就是 「TypeScript 會自動推論變數的型別嗎?」。答案是肯定的,而且推論的結果往往足以滿足大多數開發需求,讓我們可以在不寫冗長型別註記的情況下,仍然享受到靜態型別檢查的好處。

掌握 let / const 的型別推論,不僅能提升開發效率,還能減少因型別不一致而產生的 bug。特別是 const,它的推論行為與 let 有所不同,了解兩者的差異是寫出安全、可維護程式碼的關鍵。


核心概念

1. 基本推論規則

當我們以 letconst 宣告變數,且 沒有手動指定型別 時,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 則會把字面量提升為一般型別(例如 numberstring)。

2. letconst 的差異

觀點 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 可隨時變動,型別保持為 numbergreeting 雖然是字串,但因使用 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 constlet b = a as const;
函式參數缺乏型別 直接把參數傳入未指定型別的函式會得到 any,削弱型別檢查。 為函式參數明確註記型別或使用 泛型
使用 any 逃避推論 把變數宣告為 any 會失去 TypeScript 的所有好處。 盡量避免 any,改用 unknown 或具體型別。

最佳實踐

  1. 預設使用 const:除非確定需要重新指派,否則使用 const 讓編譯器自動保留字面量型別。
  2. 適度使用 as const:在需要「凍結」資料(如設定檔、列舉值)時使用,避免在普通變數上濫用。
  3. 初始化即賦值:讓 TypeScript 能在宣告時就完成推論,提升型別安全。
  4. 結合 readonly 介面:對於大型物件,只想凍結部分屬性時,使用介面或型別別名定義 readonly 屬性。
  5. 利用型別守衛(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"

總結

  • letconst 皆支援 型別推論,但 const 會保留更精確的 字面量型別,讓程式在編譯階段就能捕捉到不符合預期的值。
  • 使用 as const 可以把 let、物件、陣列 也提升為只讀且保留字面量型別的形式,適合「凍結」不變資料。
  • 了解推論的限制(未初始化、any、聯合型別)以及常見陷阱,能讓我們寫出更安全、可維護的程式碼。
  • 在實務開發中,預設使用 const、適度使用 as const、及時賦值,是提升 TypeScript 效益的最佳策略。

掌握了 let / const 的型別推論之後,你將能在 減少冗餘型別註記 的同時,仍保有 強大的靜態檢查,讓前端專案更穩定、更易於維護。祝你在 TypeScript 的旅程中寫出更乾淨、更安全的程式碼!