TypeScript 進階型別操作:as const
簡介
在日常的前端開發中,我們常常需要把一段字面量(literal)資料 固定 成不可變的型別,讓 TypeScript 能夠正確推斷出最精確的型別資訊。as const 正是為了這個需求而設計的:它會把一個表達式 斷言為只讀(readonly)且 字面量型別(literal type),從而提升型別安全、減少冗餘的型別宣告。
為什麼重要?
- 防止意外變更:使用
as const後,資料會被視為 immutable,編譯器會阻止任何嘗試修改的行為。- 提升型別推論精確度:原本會被推斷為寬鬆的
string[]、number、boolean,會變成具體的"foo"、42、true,讓後續的型別檢查更嚴謹。- 減少冗長的型別宣告:不需要手動寫
type MyConst = readonly ["a", "b", "c"];,只要as const一句即可。
本篇文章將深入探討 as const 的運作原理、常見使用情境、可能的陷阱,以及最佳實踐,幫助你在實務開發中善用這個強大的型別工具。
核心概念
1. 基本語法與型別推論
as const 是一個 型別斷言(type assertion),語法上寫在表達式的最後:
const obj = {
name: "Alice",
age: 30,
} as const;
上例中,若沒有 as const,obj 的型別會被推斷為:
{
name: string;
age: number;
}
加上 as const 後,型別變為:
{
readonly name: "Alice";
readonly age: 30;
}
readonly:屬性變成只讀,無法再被賦值。- 字面量型別:
"Alice"、30成為具體的型別,而非寬鬆的string、number。
小技巧:
as const會遞迴套用到所有子層級,陣列、物件、甚至巢狀的物件都會被一次性處理。
2. as const 與陣列
陣列是最常見的使用情境之一:
// 沒有 as const
const colors = ["red", "green", "blue"];
// 推斷為 string[]
/* 加上 as const */
const colors = ["red", "green", "blue"] as const;
// 推斷為 readonly ["red", "green", "blue"]
此時 colors 的型別是 只讀的 tuple,每個元素都有自己的字面量型別。這讓我們可以安全地使用 colors[0] 取得 "red",而不必擔心它被當成一般的 string。
範例 1:使用 as const 產生嚴格的 enum 替代方案
// 定義一組固定的字串集合
const STATUS = ["idle", "loading", "success", "error"] as const;
// 從字面量陣列取得 union 型別
type Status = typeof STATUS[number]; // "idle" | "loading" | "success" | "error"
function setStatus(s: Status) {
console.log(`Current status: ${s}`);
}
setStatus("loading"); // ✅ 正確
// setStatus("unknown"); // ❌ 編譯錯誤
3. as const 與巢狀物件
as const 會遞迴套用至所有層級,讓巢狀物件也變成只讀且保留字面量型別。
範例 2:巢狀設定檔
const CONFIG = {
api: {
url: "https://api.example.com",
timeout: 5000,
},
featureFlags: {
darkMode: true,
beta: false,
},
} as const;
// 取得深層型別
type ApiUrl = typeof CONFIG["api"]["url"]; // "https://api.example.com"
type DarkModeFlag = typeof CONFIG["featureFlags"]["darkMode"]; // true
// 嘗試修改會報錯
// CONFIG.api.timeout = 3000; // ❌ TypeError: Cannot assign to 'timeout' because it is a read-only property.
4. 把 as const 與函式結合
有時候我們想把一段字面量資料傳遞給函式,同時保留最精確的型別資訊。可以在呼叫時直接使用 as const:
function createAction<T extends string, P>(type: T, payload: P) {
return { type, payload } as const;
}
/* 使用時加上 as const,讓 type 成為字面量 */
const inc = createAction("increment", { amount: 1 } as const);
// inc 的型別為 { readonly type: "increment"; readonly payload: { readonly amount: 1 } }
如果不加 as const,payload.amount 會被推斷為 number,失去字面量型別的好處。
5. 與映射型別(Mapped Types)配合
as const 常與映射型別一起使用,產生「只讀」的「字面量」結構。例如,從一個字面量陣列產生只讀的物件映射:
const KEYS = ["id", "name", "email"] as const;
type ObjFromKeys<T extends readonly (keyof any)[]> = {
readonly [K in T[number]]: string;
};
type User = ObjFromKeys<typeof KEYS>;
// 等同於:
// type User = {
// readonly id: string;
// readonly name: string;
// readonly email: string;
// };
這樣的寫法在建構 API 回傳型別或表單欄位映射時非常實用。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記加 as const |
直接使用字面量陣列或物件時,型別會被寬鬆推斷,導致後續檢查失效。 | 在宣告或傳遞時務必加上 as const,或使用 const assertion 變數。 |
| 與可變變數混用 | 把 as const 的結果指派給 let,仍會保留只讀屬性,但會在編譯階段產生錯誤。 |
使用 const 宣告,或在需要可變的情況下另建一個可變的拷貝。 |
在函式參數上使用 as const |
若在呼叫函式時使用 as const,可能會導致型別推斷失敗(尤其是泛型函式)。 |
確保函式的型別參數足夠寬鬆,或在函式內部使用 as const 斷言返回值。 |
與 Object.freeze 混用 |
as const 僅在編譯階段提供只讀保證,執行時仍可被修改;Object.freeze 才是真正的 runtime 防篡改。 |
若需要 runtime 防止變更,兩者一起使用:const obj = { … } as const; Object.freeze(obj); |
對大型物件使用 as const 產生過大型別 |
大型巢狀結構會產生巨量的字面量型別,編譯時間可能變長。 | 只在需要精確型別的子層使用 as const,或使用 as const satisfies(TS 5.0+)限制型別範圍。 |
最佳實踐
- 預設使用
const+as const:在宣告常量、設定檔、字面量枚舉時,直接加上as const。 - 只在需要精確型別的地方使用:避免對整個大型物件一次性
as const,只挑選關鍵層級。 - 結合
typeof產生 union 型別:type X = typeof SOME_CONST[number]是取得字面量陣列成員的常用技巧。 - 搭配
Object.freeze產生 runtime 不可變:若程式執行期間也需要防止變更,額外呼叫Object.freeze。 - 利用
as const satisfies(TS 5.0+):在保留字面量型別的同時,限制符合某個介面或型別,提升可讀性與安全性。
// 只保留字面量型別,同時檢查結構符合 ConfigSchema
const CONFIG = {
mode: "production",
debug: false,
} as const satisfies { mode: "development" | "production"; debug: boolean };
實際應用場景
1. Redux / Zustand 等狀態管理的 Action 常量
在 Redux 中,我們常會寫:
export const INCREMENT = "counter/increment" as const;
export const DECREMENT = "counter/decrement" as const;
as const 讓 INCREMENT 的型別變成字面量 "counter/increment",配合 type Action = typeof INCREMENT | typeof DECREMENT,可以在 reducer 中得到完整的型別安全。
2. API 回傳型別的映射
假設後端回傳的字串集合固定:
const ROLE = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLE[number];
前端在取得使用者角色時,只能接受 Role,避免拼寫錯誤或非法值。
3. 表單驗證規則
const VALIDATIONS = {
required: "此欄位為必填",
email: "請輸入有效的 Email",
minLength: (len: number) => `至少需要 ${len} 個字元`,
} as const;
// 取得所有鍵的 union 型別
type ValidationKey = keyof typeof VALIDATIONS; // "required" | "email" | "minLength"
這樣的寫法讓 UI 元件可以根據 ValidationKey 動態顯示對應的錯誤訊息,且編譯器會保證鍵名正確。
4. 國際化(i18n)字典
const LANG_ZH_TW = {
hello: "哈囉",
goodbye: "再見",
welcome: "歡迎使用",
} as const;
type ZhTwKey = keyof typeof LANG_ZH_TW; // "hello" | "goodbye" | "welcome"
在取字串時:
function t(key: ZhTwKey): string {
return LANG_ZH_TW[key];
}
t("welcome"); // ✅
使用 as const 確保字典鍵不會被誤寫或在編譯階段遺失。
5. 測試資料(Test Fixtures)
測試中常需要固定的測試資料:
export const MOCK_USER = {
id: 1,
name: "測試者",
role: "admin",
} as const;
// 在測試斷言中直接利用字面量型別
expect(result.role).toBe(MOCK_USER.role);
因為 MOCK_USER.role 為字面量 "admin",即使在測試中不小心改寫,也會在編譯階段被捕捉。
總結
as const 是 TypeScript 中一個簡潔卻威力巨大的型別斷言工具。它讓我們能夠:
- 將字面量資料升級為只讀且精確的字面量型別,提升型別安全。
- 在不額外撰寫介面或型別別名的情況下,快速得到 union、tuple、readonly 物件等複雜型別。
- 結合
typeof、映射型別與as const satisfies,打造可維護、可擴充的型別架構。
在實務開發中,善用 as const 可以減少錯誤、提升開發效率,尤其在 設定檔、枚舉、API 回傳、狀態管理、國際化字典 等場景。只要注意避免對過大結構一次性斷言、配合 Object.freeze 保障 runtime 安全,就能發揮它最大的價值。
最終建議:在任何固定不變且需要精確型別的字面量資料上,都先考慮使用
as const;當型別需要更嚴格的結構驗證時,結合satisfies或自訂介面,以取得最佳的型別體驗與程式碼可讀性。祝你在 TypeScript 的型別世界中玩得開心! 🚀