Enum trong TypeScript là một tập hằng số có tên. enum Direction { Up, Down, Left, Right } tạo ra cùng lúc một kiểu, Direction, và một đối tượng lúc chạy (runtime) mà bạn truy cập các thành viên qua Direction.Up. Các thành viên được đánh số từ 0 nếu bạn không gán giá trị, còn enum chuỗi gán cho mỗi thành viên một chuỗi dễ đọc.
Enum là một trong số ít tính năng của TypeScript không chỉ là kiểu: khi code được biên dịch, enum trở thành một đối tượng JavaScript thật.
Enum số
Khi không có giá trị khởi tạo, các thành viên nhận 0, 1, 2 và cứ thế tiếp tục. Gán một số cho thành viên đầu tiên thì các thành viên sau sẽ đếm tiếp từ đó. Bạn cũng có thể đặt giá trị rõ ràng cho từng thành viên, đây là lựa chọn an toàn khi các con số được lưu vào cơ sở dữ liệu hoặc gửi qua mạng.
Dựa vào việc tự đánh số là ổn với những giá trị không bao giờ rời khỏi chương trình. Nếu thứ tự thành viên có thể thay đổi và các con số được lưu ở đâu đó, thì chèn một thành viên vào giữa sẽ âm thầm đánh số lại mọi thành viên phía sau.
Enum được biên dịch thành gì
Kiểu bị xóa đi, nhưng enum thì không. Đây là đoạn JavaScript mà TypeScript sinh ra cho một enum số và một enum chuỗi:
enum Direction { Up, Down, Left, Right }
enum Status { Active = "ACTIVE", Inactive = "INACTIVE" }
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
var Status;
(function (Status) {
Status["Active"] = "ACTIVE";
Status["Inactive"] = "INACTIVE";
})(Status || (Status = {}));
Direction["Up"] = 0 trả về 0, nên Direction[0] = "Up" được gán ngay trong cùng câu lệnh. Vì vậy enum số ánh xạ theo cả hai chiều: từ tên sang số và từ số ngược về tên. Đó chính là ánh xạ ngược (reverse mapping). Enum chuỗi chỉ ánh xạ từ tên sang giá trị.
Đối tượng Direction được in ra có tám khóa: bốn tên và bốn con số. Điều đó trở nên quan trọng ngay khi bạn duyệt qua nó.
Enum chuỗi
Mỗi thành viên của enum chuỗi cần một giá trị chuỗi được gán rõ ràng. Các giá trị xuất hiện nguyên vẹn trong log, JSON và cơ sở dữ liệu, nên enum chuỗi dễ gỡ lỗi hơn enum số.
Enum chuỗi có một điểm mang tính danh nghĩa (nominal) khiến nhiều người bất ngờ: không thể gán một chuỗi thường cho nó, kể cả khi nội dung trùng với giá trị của một thành viên.
index.ts(7,5): error TS2820: Type '"ACTIVE"' is not assignable to type 'Status'. Did you mean 'Status.Inactive'?
(Gợi ý trong thông báo chỉ là phỏng đoán của trình biên dịch và ở đây thì sai; cách sửa đúng là Status.Active.) Theo chiều ngược lại, một giá trị Status có thể dùng ở bất cứ đâu cần string. Khi giá trị đến dưới dạng chuỗi, từ JSON hay một biểu mẫu, hãy chuyển đổi chúng bằng một phép kiểm tra như trong phần kiểm tra giá trị bên dưới.
Dùng enum làm kiểu
Tên enum là một kiểu có các giá trị là những thành viên của nó. Kết hợp với switch, TypeScript kiểm tra rằng mọi thành viên đều được xử lý khi hàm phải trả về một giá trị:
Nếu thêm một thành viên mới vào Shape mà không có case mới, sides sẽ không biên dịch được nữa với lỗi TS2366, Function lacks ending return statement and return type does not include 'undefined'. Trang switch giới thiệu cách kiểm tra đầy đủ chặt chẽ hơn dựa trên never.
Mấy dòng cuối cho thấy một điểm yếu thật sự của enum số. Một số literal không khớp thành viên nào, const level: Level = 99, là lỗi biên dịch (TS2322), nhưng mọi giá trị có kiểu number đều được chấp nhận, nên 57 vẫn lọt qua. Enum chuỗi không có lỗ hổng này.
Duyệt qua một enum
Lúc chạy, enum là một đối tượng, nên Object.keys, Object.values và Object.entries đều dùng được. Với enum chuỗi, chúng trả về đúng các thành viên. Với enum số, chúng trả về cả các mục ánh xạ ngược, và bạn cần lọc chúng ra:
Để khai báo một biến có kiểu "một trong các tên thành viên của enum", dùng keyof typeof Direction, tức là union "Up" | "Down" | "Left" | "Right". Khi đó Direction[name] tra cứu giá trị với đầy đủ an toàn kiểu.
Enum chuỗi không có ánh xạ ngược, nên muốn lấy tên của một thành viên từ giá trị, bạn tìm trong các mục: Object.entries(Status).find(([, v]) => v === "ACTIVE")?.[0] cho ra "Active", hoặc undefined khi không thành viên nào có giá trị đó.
Kiểm tra một giá trị có nằm trong enum không
Dữ liệu từ bên ngoài chương trình là một string hoặc number thường. Một type guard kiểm tra nó với các giá trị của enum và thu hẹp nó về kiểu enum:
Tránh dùng raw as Status với đầu vào không đáng tin: phép ép kiểu vẫn biên dịch được, nhưng lúc chạy không có gì được kiểm tra, nên "DELETED" sẽ đi khắp chương trình với kiểu Status như thể hợp lệ.
const enum
const enum yêu cầu trình biên dịch xóa enum và viết giá trị của từng thành viên vào chỗ nó được dùng. Lúc chạy không có đối tượng nào, nên không thể duyệt hay ánh xạ ngược.
const enum tiết kiệm vài byte và một lần tra cứu thuộc tính, nhưng nó phụ thuộc vào việc trình biên dịch thấy được khai báo của enum khi biên dịch mọi tệp dùng đến nó. Các công cụ chuyển mã từng tệp một, như Babel và swc, không thấy được một const enum khai báo ở tệp khác; type stripping của Node từ chối const enum giống như mọi enum khác; và khi bật isolatedModules hoặc verbatimModuleSyntax, TypeScript báo lỗi TS2748 nếu bạn dùng một const enum từ tệp khai báo. Phần lớn code ứng dụng không cần đến const enum.
Enum, union type hay đối tượng as const
Có ba cách phổ biến để định nghĩa một tập giá trị cố định:
enum | Union các literal | Đối tượng as const | |
|---|---|---|---|
| Tồn tại lúc chạy | có, một đối tượng | không | có, một đối tượng thường |
| Duyệt các giá trị | Object.values (enum số: cần lọc) | không, không có gì để duyệt | Object.values |
Nhận chuỗi thường "red" | không (enum chuỗi) | có | có |
Truy cập theo tên X.Red | có | không | có |
| Ánh xạ ngược | chỉ enum số | không | không |
| Chạy được với type stripping của Node | không | có | có |
Được erasableSyntaxOnly cho phép | không | có | có |
| Cú pháp cần học thêm | quy tắc enum, const enum | không có | mẫu typeof |
Nhiều nhóm giờ mặc định dùng union các chuỗi literal, và chuyển sang đối tượng as const khi cần giá trị lúc chạy (để duyệt hoặc dựng một danh sách thả xuống). Lý do: union là kiểu thuần túy và biến mất khỏi đầu ra; chúng nhận chuỗi thường mà JSON và API gửi về; còn enum là phần duy nhất của TypeScript thường ngày không phải "JavaScript cộng với các kiểu có thể xóa".
Điểm cuối cùng đó giờ đã có ý nghĩa thực tế. Node chạy trực tiếp tệp .ts bằng cách bỏ đi các kiểu, và enum không phải thứ nó bỏ đi được:
node status.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
Cờ --experimental-transform-types của Node giúp enum chạy được, còn tùy chọn trình biên dịch erasableSyntaxOnly báo mọi enum là lỗi TS1294, This syntax is not allowed when 'erasableSyntaxOnly' is enabled., nên một dự án có thể cấm enum ngay từ đầu. Xem chạy TypeScript để biết type stripping hoạt động thế nào. Không điều gì ở đây khiến enum trở nên sai: code biên dịch bằng tsc hoặc một bundler vẫn chạy enum tốt, và một codebase đã dùng enum chẳng được lợi bao nhiêu khi chuyển đổi.
Câu hỏi thường gặp
Enum trong TypeScript là gì?
Một tập hằng số có tên, vừa là kiểu vừa là đối tượng lúc chạy: enum Direction { Up, Down } cho phép bạn viết Direction.Up và dùng Direction làm kiểu của tham số. Khác với phần lớn tính năng của TypeScript, enum không bị xóa đi: nó được biên dịch thành một đối tượng JavaScript tồn tại lúc chạy.
Làm sao để duyệt qua một enum trong TypeScript?
Với enum chuỗi, Object.values(MyEnum) cho ra các giá trị và Object.keys(MyEnum) cho ra các tên. Enum số còn chứa các mục ánh xạ ngược ("0": "Up"), nên bạn cần lọc chúng ra: Object.keys(Direction).filter((k) => isNaN(Number(k))) chỉ trả về các tên. Không thể duyệt một const enum, vì nó không tồn tại lúc chạy.
Làm sao để chuyển một chuỗi thành giá trị enum trong TypeScript?
Kiểm tra chuỗi đó với các giá trị của enum trong một type guard: function isStatus(s: string): s is Status { return (Object.values(Status) as string[]).includes(s); }. Sau khi kiểm tra, s có kiểu Status. Một phép s as Status đơn thuần vẫn biên dịch được nhưng không kiểm tra gì lúc chạy.
Nên dùng enum hay union type trong TypeScript?
Nhiều nhóm thích dùng union các chuỗi literal (type Status = "active" | "inactive"), hoặc một đối tượng as const khi cần cả giá trị lúc chạy. Union bị xóa hoàn toàn khi biên dịch, chạy được với type stripping có sẵn của Node và tùy chọn erasableSyntaxOnly, và nhận chuỗi thường như "active". Enum cũng không có gì sai, nhất là trong những codebase đã dùng sẵn.
Enum và const enum khác nhau thế nào?
Enum thường được biên dịch thành một đối tượng mà bạn có thể duyệt và tra cứu lúc chạy. const enum bị xóa khi biên dịch và mỗi chỗ dùng được thay bằng giá trị của nó (Size.Large thành 2), nên không tốn gì lúc chạy, nhưng không duyệt được, và các công cụ biên dịch từng tệp một sẽ hạn chế cách dùng nó.