Ba tháng trước, tôi đã gửi một pull request mà tôi nghĩ là hoàn toàn hợp lý.
Tôi đã tạo một enumUserRolemới để xử lý hệ thống phân quyền của chúng tôi.
TypeScript sạch sẽ, an toàn kiểu dữ liệu, và đúng chuẩn.
Đánh giá từ kỹ sư cấp cao trả về với một nhận xét:“Vui lòng không sử dụng enum.”
Tôi đã bối rối.
Enum có trong sổ tay TypeScript.
Chúng được dạy trong mọi khóa học.
Các codebase lớn sử dụng chúng.
Có gì sai với enum?
Sau đó anh ấy cho tôi xem đầu ra JavaScript đã được biên dịch.
Tôi đã xóa mọi enum khỏi codebase của chúng tôi chiều hôm đó.
Bài viết này giải thích tại sao enum TypeScript là một trong những tính năng bị hiểu lầm nhiều nhất của ngôn ngữ—và tại sao bạn có lẽ nên ngừng sử dụng chúng.
Nội dung chính
Phần 1:
Ảo Tưởng Về Enum
TypeScript tự quảng cáo là “JavaScript với cú pháp cho kiểu dữ liệu”.
Lời hứa rất đơn giản:
viết TypeScript, có được tính an toàn kiểu dữ liệu, biên dịch thành JavaScript sạch.
Đối với hầu hết các tính năng TypeScript, điều này là đúng.
Interface?
Bị xóa.
Chú thích kiểu?
Bị xóa.
Generics?
Bị xóa.
Enum?
Chúng trở thành mã thực thi tại thời gian chạy.
Sự khác biệt cơ bản này khiến enum trở thành một ngoại lệ trong TypeScript—và một cái bẫy cho các nhà phát triển không hiểu mô hình biên dịch.
Ví Dụ Đơn Giản
Hãy bắt đầu với một thứ gì đó vô hại:
enumStatus{Active="ACTIVE",Inactive="INACTIVE",Pending="PENDING"}functiongetUserStatus():Status{returnStatus.Active}Trông sạch sẽ, phải không?
Đây là những gì thực sự được gửi đến người dùng của bạn:
varStatus;(function(Status){Status["Active"]="ACTIVE";Status["Inactive"]="INACTIVE";Status["Pending"]="PENDING";})(Status||(Status={}));functiongetUserStatus(){returnStatus.Active;}Đó là9 dòng JavaScriptcho 5 dòng TypeScript.
Nhưng chờ đã—nó còn tệ hơn nữa.
Phần 2:
Cơn Ác Mộng Với Enum Số
Enum chuỗi đã tệ.
Enum số là một thảm họa.
enumRole{Admin,User,Guest}Bạn có thể mong đợi điều này biên dịch thành một thứ gì đó đơn giản.
Có lẽconst Role = { Admin: 0, User: 1, Guest: 2 }.
Đây là những gì bạn thực sự nhận được:
varRole;(function(Role){Role[Role["Admin"]=0]="Admin";Role[Role["User"]=1]="User";Role[Role["Guest"]=2]="Guest";})(Role||(Role={}));Chuyện gì đang xảy ra ở đây?
TypeScript đang tạo raánh xạ ngược.
Đối tượng được biên dịch trông như thế này:
{Admin:0,User:1,Guest:2,0:"Admin",1:"User",2:"Guest"}Điều này cho phép bạn làm:Role[0] // "Admin"
Câu hỏi:
Bạn đã bao giờ cần tính năng này chưa?
Trong năm năm phát triển TypeScript chuyên nghiệp, tôi chưa bao giờ một lần cần tra cứu tên enum từ giá trị số của nó.
Chưa một lần.
Thế mà tôi đã gửi đoạn mã thừa này đến production hàng trăm lần.
Phần 3:
Vấn Đề Với Tree-Shaking
Các công cụ đóng gói hiện đại như Webpack, Rollup và Vite có khả năng tree-shaking tinh vi.
Chúng có thể loại bỏ mã không sử dụng với độ chính xác như phẫu thuật.
Trừ khi bạn đang sử dụng enum.
Vấn Đề
// types.tsexportenumStatus{Active="ACTIVE",Inactive="INACTIVE",Pending="PENDING",Archived="ARCHIVED",Deleted="DELETED"}// app.tsimport{Status}from'./types'constcurrentStatus=Status.ActiveĐiều bạn muốn:Chỉ chuỗi"ACTIVE"trong bundle của bạn.
Bạn nhận được:Toàn bộ đối tượng enumStatuscộng với trình bao bọc IIFE.
Enum không thể được tree-shaken vì chúng là các cấu trúc thời gian chạy.
Ngay cả khi bạn chỉ sử dụng một giá trị, bạn vẫn nhận được tất cả chúng.
Nhân điều này trên hàng chục enum trong một ứng dụng thực tế, và bạn đang gửi đi hàng kilobyte mã không cần thiết.
Phần 4:
Giải Pháp Thay Thế Tốt Hơn
Vậy nếu enum có vấn đề, chúng ta nên sử dụng gì thay thế?
Giải pháp 1:
Đối tượng Const với ‘as const’
constStatus={Active:"ACTIVE",Inactive:"INACTIVE",Pending:"PENDING"}asconstJavaScript đã biên dịch:
constStatus={Active:"ACTIVE",Inactive:"INACTIVE",Pending:"PENDING"}Thế thôi.
Không IIFE.
Không chi phí thời gian chạy.
Chỉ là một đối tượng đơn giản.
Tạo Kiểu
typeStatus=typeofStatus[keyoftypeofStatus]// Mở rộng thành:
type Status = "ACTIVE" | "INACTIVE" | "PENDING"Bây giờ bạn có:
- ✅ Một đối tượng thời gian chạy cho các giá trị
- ✅ Một kiểu thời gian biên dịch để kiểm tra kiểu
- ✅ Không có chi phí biên dịch
- ✅ Có thể tree-shaken (nếu trình đóng gói của bạn hỗ trợ)
Cách sử dụng
// Hoạt động chính xác như enum:functionsetStatus(status:Status){console.log(status)}setStatus(Status.Active)// ✅ Hợp lệsetStatus("ACTIVE")// ✅ Hợp lệ (nó chỉ là một chuỗi)setStatus("INVALID")// ❌ Lỗi kiểuPhần 5:
Lợi Thế An Toàn Kiểu
Đây là nơi trở nên thú vị:đối tượng const cung cấp độ an toàn kiểu TỐT HƠN enum.
Vấn đề với Enum
enumColor{Red=0,Blue=1}enumStatus{Inactive=0,Active=1}functionsetColor(color:Color){console.log(`Color:${color}`)}// Điều này biên dịch thành công:setColor(Status.Active)// Không lỗi!Tại sao?Bởi vì enum TypeScript sử dụng kiểu cấu trúc.
CảColorvàStatusđều là số, vì vậy TypeScript coi chúng tương thích.
Điều này đã được biên dịch và đưa vào sản xuất.
Nó gây ra một lỗi mất hàng giờ để gỡ lỗi.
Giải pháp Đối tượng
constColor={Red:"RED",Blue:"BLUE"}asconstconstStatus={Inactive:"INACTIVE",Active:"ACTIVE"}asconsttypeColor=typeofColor[keyoftypeofColor]functionsetColor(color:Color){console.log(`Color:${color}`)}// Lỗi kiểu:setColor(Status.Active)// ❌ Kiểu '"ACTIVE"' không thể gán cho kiểu '"RED" | "BLUE"'Phương pháp đối tượng const sử dụngkiểu chữ, là các giá trị chuỗi chính xác.
TypeScript bắt lỗi tại thời điểm biên dịch.
Đối tượng const cung cấp kiểm tra kiểu chặt chẽ hơn enum.
Phần 6:
Lộ Trình Di Chuyển
Đã thuyết phục?
Đây là cách di chuyển các enum hiện có.
Bước 1:
Xác định Enum Chuỗi
Đây là những cái dễ di chuyển nhất:
// TrướcenumStatus{Active="ACTIVE",Inactive="INACTIVE"}// SauconstStatus={Active:"ACTIVE",Inactive:"INACTIVE"}asconsttypeStatus=typeofStatus[keyoftypeofStatus]Bước 2:
Chuyển đổi Enum Số
Đối với enum số, bạn cần giữ lại các số:
// TrướcenumHttpStatus{OK=200,NotFound=404,ServerError=500}// SauconstHttpStatus={OK:200,NotFound:404,ServerError:500}asconsttypeHttpStatus=typeofHttpStatus[keyoftypeofHttpStatus]Bước 3:
Cập nhật cách sử dụng
Tin tốt?
Cách sử dụng hầu như không thay đổi:
// Cả hai đều hoạt động giống nhau:conststatus1:Status=Status.Activeconststatus2:HttpStatus=HttpStatus.OK// Pattern matching vẫn hoạt động:switch(status){caseStatus.Active:// ...caseStatus.Inactive:// ...}Bước 4:
Xử lý các trường hợp đặc biệt
Nếu bạn đang sử dụng tra cứu ngược (hiếm), bạn cần tạo một ánh xạ ngược rõ ràng:
constHttpStatus={OK:200,NotFound:404}asconst// Chỉ tạo ánh xạ ngược nếu cần:constHttpStatusNames={200:"OK",404:"NotFound"}asconstHttpStatusNames[200]// "OK"Phần 7:
Ngoại lệ duy nhất
Có khi nào có lý do chính đáng để sử dụng enum không?
Có thể:
const enum
constenumDirection{Up,Down,Left,Right}constmove=Direction.UpBiên dịch thành:
constmove=0/
* Direction.Up */Const enum đượcinlinetại thời điểm biên dịch.
Chúng không tạo ra các đối tượng runtime.
Tuy nhiên:
- Chúng không hoạt động vớiisolatedModules(bắt buộc cho Babel, esbuild, SWC)
- Chúng đang bị loại bỏ để ủng hộpreserveConstEnums
- Chúng phức tạp hơn so với việc chỉ sử dụng objects
Đề xuất của tôi:Ngay cả với const enum, hãy chỉ sử dụng objects.
Đơn giản hơn là tốt hơn.
Phần 8:
Tác động thực tế
Khi chúng tôi di chuyển codebase từ enum sang const objects, đây là những gì xảy ra:
Trước khi di chuyển
- Enum trong codebase:47
- Kích thước bundle:2.4 MB (minified)
- Code liên quan đến enum trong bundle:~14 KB
Sau khi di chuyển
- Enum trong codebase:0
- Kích thước bundle:2.388 MB (minified)
- Tiết kiệm:12 KB
“Chỉ 12KB?”
Đúng, nhưng:
- Đó là 12KB chúng ta không cần phải gửi, phân tích cú pháp hoặc thực thi
- Tính an toàn kiểu được cải thiện (chúng tôi phát hiện 3 lỗi trong quá trình di chuyển)
- Code trở nên dễ đọc hơn (nó chỉ là JavaScript)
- Các nhà phát triển mới làm quen nhanh hơn (ít đặc điểm kỳ lạ của TypeScript hơn)
Cải thiện trải nghiệm nhà phát triển
- Biên dịch nhanh hơn:TypeScript không cần tạo code enum
- Hiệu suất IDE tốt hơn:Ít cấu trúc runtime cần theo dõi hơn
- Debug dễ dàng hơn:Console log hiển thị giá trị thực tế, không phải tham chiếu enum
- Mô hình tinh thần đơn giản hơn:Ít hơn một tính năng đặc thù của TypeScript cần ghi nhớ
Phần 9:
Các phản đối thường gặp
“Nhưng enum có trong tài liệu TypeScript!”
Namespace cũng vậy, và chúng cũng được coi là di sản.
Nhóm TypeScript đã thừa nhận rằng enum là một sai lầm, nhưng họ không thể xóa chúng mà không gây ra breaking changes.
“Toàn bộ codebase của tôi sử dụng enum!”
Việc di chuyển rất đơn giản và có thể được thực hiện từng bước.
Bắt đầu với code mới, di chuyển code cũ trong quá trình refactor.
“Enum rõ ràng hơn!”
// EnumenumStatus{Active="ACTIVE"}// ObjectconstStatus={Active:"ACTIVE"}asconstSự khác biệt là tối thiểu.
Phiên bản object thực sự phù hợp với JavaScript hơn.
“Tôi cần cả type và value!”
Bạn có cả hai với pattern const object:
constStatus={Active:"ACTIVE"}asconst// Runtime valuetypeStatus=typeofStatus[keyoftypeofStatus]// Compile-time type“Còn về JSON serialization thì sao?”
Enum vẫn serialize về giá trị cơ bản của chúng:
enumStatus{Active="ACTIVE"}JSON.stringify({status:Status.Active})// {"status":"ACTIVE"}Tương tự như:
constStatus={Active:"ACTIVE"}asconstJSON.stringify({status:Status.Active})// {"status":"ACTIVE"}Không có sự khác biệt.
Phần 10:
Quan điểm triết học
Phương châm của TypeScript là “JavaScript có thể mở rộng.” Code TypeScript tốt nhất là code trông giống JavaScript nhưng có chú thích kiểu.
Enum vi phạm nguyên tắc này.
Chúng là một cấu trúc chỉ có trong TypeScript, tạo ra code runtime và hoạt động khác với bất kỳ thứ gì trong JavaScript.
Khi nghi ngờ, hãy ưu tiên các thành ngữ JavaScript với kiểu TypeScript hơn là các tính năng đặc thù của TypeScript.
TypeScript tốt:
constStatus={Active:"ACTIVE"}asconsttypeStatus=typeofStatus[keyoftypeofStatus]Đây là JavaScript (một đối tượng) với kiểu TypeScript.
Nó có thể mở rộng.
Nó quen thuộc.
Nó hoạt động ở mọi nơi.
TypeScript đáng ngờ:
enumStatus{Active="ACTIVE"}Đây là cú pháp đặc thù TypeScript tạo ra code runtime không mong đợi.
Kết luận:
Hãy chuyển đổi
Enum TypeScript từng là một ý tưởng hay vào năm 2012.
Đến năm 2025, chúng ta có những lựa chọn tốt hơn.
Lý do chống lại enum:
- ❌ Tạo ra code runtime không mong đợi
- ❌ Không thể tree-shake
- ❌ Tạo ánh xạ ngược không ai sử dụng
- ❌ Tính an toàn kiểu yếu hơn so với kiểu literal
- ❌ Cú pháp đặc thù TypeScript
Lý do ủng hộ đối tượng const:
- ✅ Không có chi phí runtime
- ✅ Có thể tree-shake
- ✅ Chỉ là JavaScript
- ✅ Tính an toàn kiểu mạnh hơn
- ✅ Hoạt động ở mọi nơi
Lần tới khi bạn định dùng enum, hãy sử dụng đối tượng const thay thế.
Bundle của bạn sẽ nhỏ hơn.
Kiểu của bạn sẽ chặt chẽ hơn.
Code của bạn sẽ rõ ràng hơn.
Ngừng sử dụng enum.
Bắt đầu sử dụng đối tượng.
Hướng dẫn tham khảo nhanh
Di chuyển String Enum
// ❌ Cách cũenumStatus{Active="ACTIVE",Inactive="INACTIVE"}// ✅ Cách mớiconstStatus={Active:"ACTIVE",Inactive:"INACTIVE"}asconsttypeStatus=typeofStatus[keyoftypeofStatus]Di chuyển Numeric Enum
// ❌ Cách cũenumPriority{Low=1,Medium=2,High=3}// ✅ Cách mớiconstPriority={Low:1,Medium:2,High:3}asconsttypePriority=typeofPriority[keyoftypeofPriority]Kiểu trợ giúp để tái sử dụng
// Tạo kiểu trợ giúp có thể tái sử dụngtypeValueOf<T>=T[keyofT]constStatus={Active:"ACTIVE",Inactive:"INACTIVE"}asconsttypeStatus=ValueOf<typeofStatus>Đọc thêm
- TypeScript Handbook:
 Enums
- TypeScript Deep Dive:
 Enums
- Why TypeScript Enums Suckby Matt Pocock
- TypeScript const assertions
 
             
		