TypeScript 課程 – 變數與常數宣告(Variables)
主題:常數字面值型別(Literal Inference)
簡介
在 JavaScript 中,我們習慣直接寫 const foo = 1、let bar = "hello",而不會去思考 這個值的型別 會是什麼。
進入 TypeScript 後,編譯器會自動推斷變數的型別,這個過程稱為 型別推斷(type inference)。其中最常見、也是最有威力的推斷之一,就是 常數字面值型別(Literal Inference)。
透過字面值型別,我們可以讓變數的型別更精確,進而得到更嚴謹的型別檢查、編譯期錯誤提示,甚至在 IDE 裡獲得更好的自動完成與重構支援。對於寫 可維護、可預測 的程式碼而言,掌握字面值型別是每位 TypeScript 開發者的必備功力。
核心概念
1. 什麼是「字面值型別」?
字面值型別是指 直接由程式碼裡的字面值(literal)所推斷出的具體值型別。
例如:
1→ 型別為1(而非一般的number)"yes"→ 型別為"yes"(而非string)true→ 型別為true(而非boolean)
當我們把這樣的字面值賦值給 const 時,TypeScript 會把變數的型別鎖定為那個具體值;若使用 let,則會退化為寬鬆的基礎型別(number、string、boolean)。
重點:
const與字面值結合時,會得到 字面值型別;let則不會。
2. 為什麼要使用字面值型別?
| 好處 | 說明 |
|---|---|
| 更精確的型別檢查 | 只能接受特定的值,避免不小心傳入錯誤的字串或數字。 |
| 提升 IDE 體驗 | 自動完成只會顯示合法的選項,減少手打錯誤。 |
| 支援 discriminated unions(辨別聯合型別) | 在 switch、if 判斷時,編譯器能自動窄化型別。 |
| 可作為常數枚舉 | 取代傳統的 enum,讓程式碼更輕量且可 tree‑shake。 |
3. 基本範例
下面示範 const 與 let 在字面值推斷上的差異:
// 1️⃣ const 會得到字面值型別
const STATUS_OK = 200; // 型別是 200(而不是 number)
const MODE = "production"; // 型別是 "production"
const IS_ENABLED = true; // 型別是 true
// 2️⃣ let 會退化為寬鬆型別
let count = 10; // 型別是 number
let env = "development"; // 型別是 string
let flag = false; // 型別是 boolean
小技巧:如果你想要保留字面值型別,但又需要稍後修改值,可以使用
as const(稍後說明)。
4. as const – 把任何表達式變成字面值型別
有時候我們會在物件或陣列上一次性建立多個常數,這時候直接使用 const 仍會得到寬鬆的型別。as const 讓 TypeScript 把整個結構「凍結」成字面值型別。
// 沒有 as const → 物件屬性是寬鬆型別
const config1 = {
mode: "development",
debug: false,
};
// config1 的型別是 { mode: string; debug: boolean; }
// 使用 as const → 每個屬性都變成字面值型別
const config2 = {
mode: "production",
debug: false,
} as const;
// config2 的型別是 { readonly mode: "production"; readonly debug: false; }
注意:
as const會把所有屬性標記為readonly,因此無法再被重新賦值,這正符合「常數」的語意。
5. 字面值型別在聯合型別(Union Types)中的威力
當我們用字面值型別組成聯合型別時,編譯器能夠在條件分支裡自動窄化(type narrowing),讓程式碼更安全。
type Direction = "up" | "down" | "left" | "right";
function move(dir: Direction) {
if (dir === "up") {
// dir 已被窄化為字面值 "up"
console.log("向上移動");
} else if (dir === "down") {
console.log("向下移動");
} else {
// 這裡的 dir 仍然是 "left" | "right"
console.log(`向 ${dir} 移動`);
}
}
// 正確使用
move("left");
// 錯誤示範:編譯期會報錯
// move("forward"); // Error: Argument of type '"forward"' is not assignable to parameter of type 'Direction'.
6. 進階範例:使用字面值型別建構「類型安全」的 API
假設我們要寫一個簡易的 HTTP 客戶端,想要限制 method 只能是 GET、POST、PUT、DELETE。
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
interface RequestOptions {
url: string;
method: HttpMethod;
body?: any;
}
/**
* 送出 HTTP 請求
* @param opts 請求設定
*/
function request(opts: RequestOptions) {
// 這裡的 method 已經是字面值型別,IDE 會只顯示四個合法選項
console.log(`發送 ${opts.method} 請求至 ${opts.url}`);
}
// 正確使用
request({ url: "/api/users", method: "GET" });
// 錯誤示範 – 編譯期直接捕捉
// request({ url: "/api/users", method: "PATCH" }); // Error
7. 讓數字字面值成為「枚舉」的另一種寫法
在純 TypeScript 中,我們可以用 as const 搭配 typeof 產生類似 enum 的型別集合,且不會產生額外的 JS 代碼。
const STATUS = {
OK: 200,
NOT_FOUND: 404,
INTERNAL_ERROR: 500,
} as const;
// 取出所有可能的值型別
type StatusCode = typeof STATUS[keyof typeof STATUS];
// 使用
function handleResponse(code: StatusCode) {
if (code === STATUS.OK) {
console.log("成功");
} else {
console.log(`錯誤代碼:${code}`);
}
}
// 正確
handleResponse(STATUS.NOT_FOUND);
// 錯誤 – 編譯期會報錯
// handleResponse(403); // Error
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
把 let 當作 const 使用 |
let 會退化為寬鬆型別,失去字面值的精確度。 |
只在不可變的情況下使用 const,或在需要保留字面值時加入 as const。 |
忘記 as const 造成型別過寬 |
物件或陣列在沒有 as const 時,屬性會變成一般型別。 |
在宣告常量物件/陣列時,直接寫 as const,或使用 readonly 手動標註。 |
字面值型別與 enum 混用 |
enum 會產生額外的 JavaScript 代碼,與字面值型別的 tree‑shake 目標相衝突。 |
若不需要運行時的映射,盡量使用字面值型別 + as const 替代 enum。 |
| 過度使用字面值型別導致可讀性下降 | 把所有字串都寫成字面值聯合型別,會讓型別定義變得冗長。 | 把相關字面值抽離成獨立的 type,並在註解說明其語意。 |
在函式參數上使用 as const 失效 |
as const 只能在表達式層級使用,不能直接寫在參數型別上。 |
在呼叫端使用 as const,或在函式內部使用泛型 T extends readonly [...] 進行推斷。 |
最佳實踐小結
- 盡量使用
const+ 字面值:讓變數自動得到最精確的型別。 - 需要凍結物件/陣列時加
as const:保持所有屬性為字面值且只讀。 - 把字面值型別抽離成獨立
type:提升可讀性與重用性。 - 配合
union、discriminated union使用:讓條件分支自動窄化,減少手寫型別守衛。 - 避免不必要的
enum:若僅在編譯期使用,字面值型別更輕量且支援 tree‑shake。
實際應用場景
| 場景 | 為何適合使用字面值型別 |
|---|---|
| API 請求方法限制 | 只允許 `"GET" |
| 狀態碼常數 | 用 as const 建立 STATUS 物件,讓函式參數只能接受合法的狀態碼。 |
| UI 主題切換 | `type Theme = "light" |
| 表單驗證規則 | `type Rule = "required" |
| Redux/NgRx Action Types | 使用字面值字串作為 action type,讓 reducer 的 switch case 自動得到完整型別檢查。 |
| 多語系鍵值 | 以 as const 定義 TRANSLATIONS 物件,保證取值時不會寫錯鍵名。 |
總結
- 字面值型別是 TypeScript 型別推斷中最精確、最具威力的特性之一。
- 只要搭配
const或as const,就能讓變數、物件、陣列「凍結」成具體的值型別,從而得到更嚴謹的編譯期檢查。 - 在 聯合型別、辨別聯合(discriminated unions)、API 常數 等情境下,字面值型別不僅提升程式安全性,也改善 IDE 的自動完成與重構體驗。
- 使用時要留意
let的退化行為、as const的只讀限制,以及適度抽離型別以維持可讀性。
掌握了字面值型別,你的 TypeScript 程式碼將更安全、可維護,同時也能更好地發揮靜態型別的優勢。祝你在 TypeScript 的旅程中寫出更乾淨、錯誤更少的程式!