三個月前,我提交了一個我認為非常合理的拉取請求。我建立了一個新的UserRole
枚舉來處理我們的權限系統。簡潔、型別安全、符合 TypeScript 規範。
資深工程師的評審結果只有一個: “請不要使用枚舉。”
我當時很困惑。枚舉在 TypeScript 手冊裡,每門課都會講到。主流程式碼庫都在用它們。枚舉有什麼問題嗎?
然後他向我展示了編譯後的 JavaScript 輸出。
那天下午我從我們的程式碼庫中刪除了每個枚舉。
本文解釋了為什麼 TypeScript 枚舉是該語言最容易被誤解的功能之一,以及為什麼你應該停止使用它們。
TypeScript 自稱是「具有型別語法的 JavaScript」。它的承諾很簡單:寫 TypeScript,獲得型別安全,編譯為乾淨的 JavaScript。
對於大多數 TypeScript 特性來說,確實如此。接口?被刪除了。類型註解?被刪除了。泛型?被刪除了。
枚舉?它們會變成真正的執行時程式碼。
這種根本的差異使得枚舉在 TypeScript 中成為一個異常現象,並且對於不了解編譯模型的開發人員來說是一個陷阱。
讓我們從一些無辜的事情開始:
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING"
}
function getUserStatus(): Status {
return Status.Active
}
看起來很簡潔,對吧?以下是實際交付給用戶的內容:
var Status;
(function (Status) {
Status["Active"] = "ACTIVE";
Status["Inactive"] = "INACTIVE";
Status["Pending"] = "PENDING";
})(Status || (Status = {}));
function getUserStatus() {
return Status.Active;
}
也就是說,5 行 TypeScript 程式碼對應9 行 JavaScript 程式碼。
但等等——情況變得更糟了。
字串枚舉很糟糕。數字枚舉更是一場災難。
enum Role {
Admin,
User,
Guest
}
你可能會希望這段程式碼編譯起來很簡單。如const Role = { Admin: 0, User: 1, Guest: 2 }
。
以下是您實際得到的:
var Role;
(function (Role) {
Role[Role["Admin"] = 0] = "Admin";
Role[Role["User"] = 1] = "User";
Role[Role["Guest"] = 2] = "Guest";
})(Role || (Role = {}));
TypeScript 正在建立反向映射。編譯後的物件如下所示:
{
Admin: 0,
User: 1,
Guest: 2,
0: "Admin",
1: "User",
2: "Guest"
}
這允許您執行以下操作: Role[0] // "Admin"
問題:您是否曾經需要過此功能?
在五年的專業 TypeScript 開發經驗中,我從來沒有需要透過枚舉值的數值來找出枚舉名稱。一次也沒有。
然而我已經將這段額外的程式碼投入生產數百次了。
Webpack、Rollup 和 Vite 等現代化打包工具都擁有先進的 tree-shaking 功能,能夠精確地刪除無用程式碼。
除非您使用枚舉。
// types.ts
export enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
Archived = "ARCHIVED",
Deleted = "DELETED"
}
// app.ts
import { Status } from './types'
const currentStatus = Status.Active
您想要的:只是捆綁包中的字串"ACTIVE"
。
您得到的是:整個Status
枚舉物件加上 IIFE 包裝器。
枚舉無法進行 tree-shaking,因為它們是執行時構造的。即使你只使用一個值,你也會取得所有值。
在實際應用程式中將其乘以數十個枚舉,您將發送數千位元組的不必要程式碼。
那如果枚舉有問題,我們該用什麼來代替呢?
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE",
Pending: "PENDING"
} as const
編譯後的 JavaScript:
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE",
Pending: "PENDING"
}
就是這樣。沒有 IIFE。沒有運轉時開銷。只是一個簡單的物件。
type Status = typeof Status[keyof typeof Status]
// Expands to: type Status = "ACTIVE" | "INACTIVE" | "PENDING"
現在你有:
✅ 值的執行時物件
✅ 用於類型檢查的編譯時類型
✅ 零編譯開銷
✅ 可搖樹(如果你的打包工具支援的話)
// Works exactly like enums:
function setStatus(status: Status) {
console.log(status)
}
setStatus(Status.Active) // ✅ Valid
setStatus("ACTIVE") // ✅ Valid (it's just a string)
setStatus("INVALID") // ❌ Type error
有趣的是: const 物件比枚舉提供更好的類型安全性。
enum Color {
Red = 0,
Blue = 1
}
enum Status {
Inactive = 0,
Active = 1
}
function setColor(color: Color) {
console.log(`Color: ${color}`)
}
// This compiles successfully:
setColor(Status.Active) // No error!
為什麼?因為 TypeScript 枚舉使用結構化類型。 Color 和Status
都是Color
,所以 TypeScript 認為它們相容。
這段程式碼編譯並發佈到生產環境。但它引發了一個 bug,需要幾個小時才能除錯。
const Color = {
Red: "RED",
Blue: "BLUE"
} as const
const Status = {
Inactive: "INACTIVE",
Active: "ACTIVE"
} as const
type Color = typeof Color[keyof typeof Color]
function setColor(color: Color) {
console.log(`Color: ${color}`)
}
// Type error:
setColor(Status.Active) // ❌ Type '"ACTIVE"' is not assignable to type '"RED" | "BLUE"'
const 物件方法使用文字類型,即精確的字串值。 TypeScript 在編譯時捕捉錯誤。
Const 物件提供比枚舉更嚴格的類型檢查。
相信了嗎?下面是如何遷移現有枚舉的方法。
這些是最容易遷移的:
// Before
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE"
}
// After
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE"
} as const
type Status = typeof Status[keyof typeof Status]
對於數字枚舉,您需要保留數字:
// Before
enum HttpStatus {
OK = 200,
NotFound = 404,
ServerError = 500
}
// After
const HttpStatus = {
OK: 200,
NotFound: 404,
ServerError: 500
} as const
type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus]
好訊息?用法基本上保持不變:
// Both work identically:
const status1: Status = Status.Active
const status2: HttpStatus = HttpStatus.OK
// Pattern matching still works:
switch (status) {
case Status.Active:
// ...
case Status.Inactive:
// ...
}
如果您使用反向查找(很少見),則需要建立一個明確的反向映射:
const HttpStatus = {
OK: 200,
NotFound: 404
} as const
// Create reverse mapping only if needed:
const HttpStatusNames = {
200: "OK",
404: "NotFound"
} as const
HttpStatusNames[200] // "OK"
使用枚舉是否有正當理由?
可能:const 枚舉
const enum Direction {
Up,
Down,
Left,
Right
}
const move = Direction.Up
編譯為:
const move = 0 /* Direction.Up */
常量枚舉在編譯時內聯。它們不會建立執行時物件。
然而:
它們不適用於isolatedModules
(Babel、esbuild、SWC 所需)
它們已被棄用,取而代之的是preserveConstEnums
它們比僅僅使用物件更複雜
我的建議:即使是 const 枚舉,也只使用物件。越簡單越好。
當我們將程式碼庫從枚舉遷移到 const 物件時,發生了以下情況:
程式庫中的枚舉: 47
捆綁包大小: 2.4 MB(最小化)
捆綁包中與枚舉相關的程式碼: ~14 KB
程式碼庫中的枚舉: 0
捆綁包大小: 2.388 MB(最小化)
節省: 12 KB
“只有12KB?”
是的,但是:
12KB 的資料我們不需要傳送、解析或執行
類型安全性得到改善(我們在遷移過程中發現了 3 個錯誤)
程式碼變得更易讀(它只是 JavaScript)
新開發人員上手更快(TypeScript 怪癖更少)
更快的編譯: TypeScript 不需要產生枚舉程式碼
更好的 IDE 效能:需要追蹤的執行時構造更少
更容易除錯:控制台日誌顯示實際值,而不是枚舉引用
更簡單的思考模型:少記住一個 TypeScript 特有的功能
命名空間也是如此,它們也被視為遺留。 TypeScript 團隊已經承認枚舉是一個錯誤,但他們無法在不破壞變更的情況下刪除它們。
遷移過程簡單直接,可以逐步完成。從新程式碼開始,在重構過程中遷移舊程式碼。
// Enum
enum Status { Active = "ACTIVE" }
// Object
const Status = { Active: "ACTIVE" } as const
差別很小。物件版本其實更符合 JavaScript 的慣用用法。
使用 const 物件模式可以同時獲得這兩種效果:
const Status = { Active: "ACTIVE" } as const // Runtime value
type Status = typeof Status[keyof typeof Status] // Compile-time type
無論如何,枚舉都會序列化為其底層值:
enum Status { Active = "ACTIVE" }
JSON.stringify({ status: Status.Active }) // {"status":"ACTIVE"}
相同於:
const Status = { Active: "ACTIVE" } as const
JSON.stringify({ status: Status.Active }) // {"status":"ACTIVE"}
沒有差別。
TypeScript 的座右銘是「可擴展的 JavaScript」。最好的 TypeScript 程式碼是看起來像 JavaScript 但帶有類型註解的程式碼。
枚舉違反了這項原則。它們是一種 TypeScript 獨有的構造,會產生執行時間程式碼,且其行為與 JavaScript 中的任何內容都不同。
如有疑問,請優先使用具有 TypeScript 類型的 JavaScript 慣用語,而不是 TypeScript 特定的功能。
好的 TypeScript:
const Status = { Active: "ACTIVE" } as const
type Status = typeof Status[keyof typeof Status]
這是具有 TypeScript 類型的 JavaScript(一個物件)。它可擴展。它很熟悉。它在任何地方都能工作。
可疑的 TypeScript:
enum Status { Active = "ACTIVE" }
這是 TypeScript 特定的語法,會產生意外的執行時間程式碼。
TypeScript 枚舉在 2012 年似乎是個好主意。到 2025 年,我們將有更好的選擇。
❌ 產生意外的執行時間程式碼
❌ 不要搖晃樹
❌ 建立沒人使用的反向映射
❌ 型別安全性比字面量型弱
❌ TypeScript 特定的語法
✅ 零運轉時開銷
✅ 可搖樹
✅ 僅 JavaScript
✅ 更強的型別安全性
✅ 隨處可用
下次使用枚舉時,請改用 const 物件。
你的包包會更小。你的類型會更嚴格。你的程式碼會更清晰。
停止使用枚舉。開始使用物件。
// ❌ Old way
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE"
}
// ✅ New way
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE"
} as const
type Status = typeof Status[keyof typeof Status]
// ❌ Old way
enum Priority {
Low = 1,
Medium = 2,
High = 3
}
// ✅ New way
const Priority = {
Low: 1,
Medium: 2,
High: 3
} as const
type Priority = typeof Priority[keyof typeof Priority]
// Create a reusable type helper
type ValueOf<T> = T[keyof T]
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE"
} as const
type Status = ValueOf<typeof Status>
我是資深 TypeScript 開發者,擁有 5 年以上的生產級應用程式開發經驗。我透過向數百萬用戶推送不必要的枚舉程式碼,學到了這一慘痛教訓。現在,我將分享我的經驗,希望您避免重蹈覆轍。
如果您覺得這有幫助,請考慮與您的團隊分享。理解這一點的開發人員越多,我們交付的程式碼就越好。
原文出處:https://dev.to/elvissautet/the-code-review-that-changed-everything-1dp2