前言

在 TypeScript 裡,as const 是一個很常見的語法。

例如:

const DIRECTIONS = [
  'north',
  'south',
  'east',
  'west',
] as const;

前面已經用了 const,最後再多加了一個 as const

原因在於兩者作用於不同層面:

  • const 是 JavaScript 的宣告:它鎖定的是「變數綁定」,確保該變數不能被重新賦值;

  • as const 則是 TypeScript 的型別斷言,影響的是「型別推導」。它會要求 TypeScript 以最精確的方式推導值的型別,保留其字面量型別(Literal Type),並將 object 或 array 視為唯讀(Readonly)


constas const 的區別

一個普通的 const

const direction = 'north';

這裡 TypeScript 可以推斷出 direction 是:

"north"

但如果是 array:

const directions = ['north', 'south', 'east', 'west'];

TypeScript 通常會推斷成:

string[]

也就是說,TypeScript 認為這是一個「可以放任何 string 的 array」。

所以這是合法的:

directions.push('apple');

即便我們只想讓這個 array 應該只包含幾個固定選項。

這時候就可以使用 as const

const DIRECTIONS = [
  'north',
  'south',
  'east',
  'west'
] as const;

TypeScript 會把它推斷成:

readonly [
  'north',
  'south',
  'east',
  'west'
]

這時候:

DIRECTIONS.push('apple');

就會直接出現 TypeScript error。


as const 的作用

as const 主要有三個作用:

1. 保留字面量型別(Literal Type),避免型別寬化(Type Widening)

在沒有使用 as const 時,TypeScript 會根據一般使用情境進行型別推導,將字面量值(Literal Value)推導為較寬鬆的型別(Widened Type)。

例如:

const direction = 'north';

// 型別:string

雖然實際值是 ’north’,但 TypeScript 會推導為 string,表示未來可能是任何字串。

使用 as const 後:

const direction = 'north' as const;

// 型別:'north'

TypeScript 會保留最精確的型別資訊,將其推導為字面量型別(Literal Type),表示該值只能是 ’north’,而不能是其他字串。

2. 將物件屬性轉為唯讀(Readonly Properties)

const 僅限制變數本身不能被重新賦值,並不會影響 object 內部屬性的可變性。

例如:

const user = {
  name: 'Alice',
};

user.name = 'Bob'; // OK

加入 as const 後:

const user = {
  name: 'Alice',
} as const;

user.name = 'Bob'; // Error

TypeScript 會自動為所有屬性加上 readonly,使 object 結構成為不可變(Immutable)的型別。

其推導結果等同於:

{
  readonly name: 'Alice';
}

3. 將陣列推導為唯讀元組(Readonly Tuple)

一般 array 會被推導為可變長度的 array 型別:

const directions = ['north', 'south'];

// 型別:string[]

string[] 代表:

  1. 長度可變
  2. 元素可修改
  3. 可使用 push()、pop() 等方法
  4. directions.push(’east’); // OK

使用 as const 後:

const directions = ['north', 'south'] as const;

型別會變成:

readonly ['north', 'south']

此時:

  1. 長度固定
  2. 順序固定
  3. 元素不可修改
  4. 無法使用會改變內容的方法
  5. directions.push(’east’); // Error

因此,as const 不僅讓 array 成為唯讀,也將其從一般 array 提升為具有固定結構的元組。

總得來說,as const 是讓 TypeScript 不要進行寬鬆推導,而是完整保留這個值當下最精確、不可變的型別資訊。


最實用的用途:建立 Union Type

這是 as const 很常被使用到的地方。

假設我們有一組固定的 direction:

export const DIRECTIONS = [
  'north',
  'south',
  'east',
  'west',
] as const;

接著可以從這個 array 直接產生 TypeScript type:

export type Direction = (typeof DIRECTIONS)[number];

最後 Direction 就會變成:

type Direction =
  | 'north'
  | 'south'
  | 'east'
  | 'west';

因此我們可以寫:

function setDirection(direction: Direction) {
  // ...
}

setDirection('south'); // OK
setDirection('west');   // OK

setDirection('apple');    // Error

這帶來了極大的方便,因為我們只需要維護一份資料。

如果沒有這個做法,就會需要寫成:

const DIRECTIONS = [
  'north',
  'south',
  'east',
  'west',
];

type Direction =
  | 'north'
  | 'south'
  | 'east'
  | 'west';

這樣就有兩份需要同步維護的資料。

如果未來新增:

'northeast'

就必須記得同時修改 array 和 type。

而使用 as const ,則可以讓 type 自動跟著 array 改變:

const DIRECTIONS = [
  'north',
  'south',
  'east',
  'west',
] as const;

type Directions = (typeof DIRECTIONS)[number];

(typeof DIRECTIONS)[number] 是什麼?

來看到這個寫法:

(typeof DIRECTIONS)[number]

拆解看,首先:

typeof directions

會取得 DIRECTIONS 的 type:

readonly [
  'north',
  'south',
  'east',
  'west',
]

接著:

[number]

可以理解成:

取得這個 array 裡所有元素的 type。

因此最後得到:

  'north'
  | 'south'
  | 'east'
  | 'west';

也就是我們想要的 Union Type。


as const 的作用域僅限於 Type-level

Type-level 是 TypeScript 中的核心概念。

簡單來說:它是只存在於「寫程式與編譯階段」的邏輯世界,完全不會產生任何 JavaScript 執行期(Runtime)程式碼。

在 TypeScript 中:

  • Type-level   →   編譯後徹底消失 (不影響執行效能)

  • Value-level   →   轉換成 JavaScript 留下來 (在瀏覽器/Node.js 執行)

在 TypeScript 中,as const 常被誤認為具備 Runtime Immutable 的效果。然而,TypeScript 的型別約束與 JavaScript 的執行期行為必須嚴格區分:

  • as const 的本質:指示 TypeScript 編譯器將該值推導為最精確的字面值型別(Literal Type),並加上 readonly 修飾符。編譯為 JavaScript 後,as const 不會產生任何語法或機制。

  • 淺層唯讀與引用漏洞:當 as const 作用於包含外部引用的物件時,無法保證底層資料的不可變性。

例如:

cconst arr = [1, 2, 3];

const obj = {
  items: arr,
} as const;

// 1. Type-level 限制:無法將新的 reference 指派給 obj.items
obj.items = []; // Error: Cannot assign to 'items' because it is a read-only property.

// 2. Runtime 行為:原始 array 仍可被操作,導致 obj.items 的內容改變
arr.push(4); // 成功執行,obj.items 實質上變為 [1, 2, 3, 4]

因此,若需要型別安全與精確型別推導,使用 as const;而要在執行期防止篡改(Runtime Immutability),則需結合原生 JavaScript 的 Object.freeze() 或使用第三方 Immutable 函式庫。


適合使用 as const 的場景

通常會在「固定選項」這類場景使用 as const

例如:

const APPOINTMENT_STATUSES = [
  'pending',
  'confirmed',
  'completed',
  'cancelled',
  'no_show',
] as const;

或:

const BOOKING_MODES = [
  'online',
  'line',
  'phone',
] as const;

也可以使用 object:

const ROLES = {
  ADMIN: 'admin',
  STAFF: 'staff',
  CUSTOMER: 'customer',
} as const;

這時候每個 value 都會保留成 literal type。這種寫法也可以拿來建立類似 enum 的 pattern。TypeScript 官方文件也展示了使用 as const 建立 enum-like object 的方式。


總結

constas const 分別作用於不同的層級:const 負責 JavaScript 執行期的 Value-level,as const 則作用於 TypeScript 編譯期的 Type-level。

透過 as const,我們可以將 object 與 array 宣告為最嚴格的 readonly 字面值型別。進一步結合 typeof 操作符(如 type Direction = (typeof DIRECTIONS)[number]),即可直接利用資料來源反推 Union Type。這樣只需維護一份資料,省去手動同步的繁瑣,並徹底避免改漏出錯的風險。