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.


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}
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

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;}
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

Đó 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}
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

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={}));
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

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"}
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

Đ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
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

Đ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"}asconst
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

JavaScript đã biên dịch:

constStatus={Active:"ACTIVE",Inactive:"INACTIVE",Pending:"PENDING"}
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

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"
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

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ểu
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

Phầ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!
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

Tại sao?Bởi vì enum TypeScript sử dụng kiểu cấu trúc.
CảColorStatusđề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"'
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

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]
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

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]
Enter fullscreen modeExit fullscreen mode

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:// ...}
Enter fullscreen modeExit fullscreen mode

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"
Enter fullscreen modeExit fullscreen mode

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.Up
Enter fullscreen modeExit fullscreen mode

Biên dịch thành:

constmove=0/
* Direction.Up */
Enter fullscreen modeExit fullscreen mode

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:

  1. Chúng không hoạt động vớiisolatedModules(bắt buộc cho Babel, esbuild, SWC)
  2. Chúng đang bị loại bỏ để ủng hộpreserveConstEnums
  3. 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:

  1. Đó 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
  2. 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)
  3. Code trở nên dễ đọc hơn (nó chỉ là JavaScript)
  4. 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

  1. Biên dịch nhanh hơn:TypeScript không cần tạo code enum
  2. Hiệu suất IDE tốt hơn:Ít cấu trúc runtime cần theo dõi hơn
  3. Debug dễ dàng hơn:Console log hiển thị giá trị thực tế, không phải tham chiếu enum
  4. 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"}asconst
Enter fullscreen modeExit fullscreen mode

Sự 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
Enter fullscreen modeExit fullscreen mode

“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"}
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

Tương tự như:

constStatus={Active:"ACTIVE"}asconstJSON.stringify({status:Status.Active})// {"status":"ACTIVE"}
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

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]
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

Đâ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"}
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

Đâ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]
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

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]
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

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>
Vào chế độ toàn màn hìnhThoát chế độ toàn màn hình

Đọc thêm

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!