本文 AI 產出,尚未審核

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+)限制型別範圍。

最佳實踐

  1. 預設使用 const + as const:在宣告常量、設定檔、字面量枚舉時,直接加上 as const。
  2. 只在需要精確型別的地方使用:避免對整個大型物件一次性 as const,只挑選關鍵層級。
  3. 結合 typeof 產生 union 型別:type X = typeof SOME_CONST[number] 是取得字面量陣列成員的常用技巧。
  4. 搭配 Object.freeze 產生 runtime 不可變:若程式執行期間也需要防止變更,額外呼叫 Object.freeze。
  5. 利用 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 的型別世界中玩得開心! 🚀