本文 AI 產出,尚未審核

TypeScript 進階型別操作:as const


簡介

在日常的前端開發中,我們常常需要把一段字面量(literal)資料 固定 成不可變的型別,讓 TypeScript 能夠正確推斷出最精確的型別資訊。
as const 正是為了這個需求而設計的:它會把一個表達式 斷言為只讀readonly)且 字面量型別(literal type),從而提升型別安全、減少冗餘的型別宣告。

為什麼重要?

  • 防止意外變更:使用 as const 後,資料會被視為 immutable,編譯器會阻止任何嘗試修改的行為。
  • 提升型別推論精確度:原本會被推斷為寬鬆的 string[]numberboolean,會變成具體的 "foo"42true,讓後續的型別檢查更嚴謹。
  • 減少冗長的型別宣告:不需要手動寫 type MyConst = readonly ["a", "b", "c"];,只要 as const 一句即可。

本篇文章將深入探討 as const 的運作原理、常見使用情境、可能的陷阱,以及最佳實踐,幫助你在實務開發中善用這個強大的型別工具。


核心概念

1. 基本語法與型別推論

as const 是一個 型別斷言(type assertion),語法上寫在表達式的最後:

const obj = {
  name: "Alice",
  age: 30,
} as const;

上例中,若沒有 as constobj 的型別會被推斷為:

{
  name: string;
  age: number;
}

加上 as const 後,型別變為:

{
  readonly name: "Alice";
  readonly age: 30;
}
  • readonly:屬性變成只讀,無法再被賦值。
  • 字面量型別"Alice"30 成為具體的型別,而非寬鬆的 stringnumber

小技巧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 constpayload.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 constINCREMENT 的型別變成字面量 "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 的型別世界中玩得開心! 🚀