Lỗi Gặp Phải
Lỗi này xuất hiện khi bạn cố truyền hoặc gán các thành viên enum như thể chúng có thể hoán đổi cho nhau:
enum Status {
Active,
Inactive,
Pending,
}
function setStatus(s: Status.Inactive) {
// ...
}
const current = Status.Active;
setStatus(current); // ❌ Type 'Status.Active' is not assignable to type 'Status.Inactive'. ts(2322)
Lỗi tương tự xảy ra khi gán giữa các biến có kiểu cụ thể:
let a: Status.Active = Status.Active;
let b: Status.Inactive = a; // ❌ ts(2322)
Nguyên Nhân
TypeScript xem mỗi thành viên enum là một kiểu literal riêng biệt — không chỉ là một giá trị, mà là một kiểu thực sự khác nhau. Vì vậy Status.Active và Status.Inactive khác nhau hoàn toàn như true và false ở cấp độ kiểu dữ liệu.
Ở runtime? Chúng chỉ là các số (0 và 1). Nhưng trình kiểm tra kiểu không quan tâm đến điều đó. Một biến được khai báo kiểu Status.Inactive sẽ chỉ chấp nhận Status.Inactive — không có ngoại lệ.
Numeric enum còn có thêm một điểm đặc biệt đáng lưu ý: chúng chấp nhận các số nguyên thuần túy (nên let s: Status = 0 vẫn biên dịch được). Tuy nhiên các thành viên enum vẫn không thể gán cho nhau khi kiểu của chúng khác nhau.
Cách Sửa 1: Mở Rộng Kiểu Thành Toàn Bộ Enum
Chín lần trong mười, đây là cách sửa đúng. Nếu hàm nên chấp nhận bất kỳ giá trị Status nào, hãy khai báo tham số kiểu Status thay vì Status.Inactive:
// ❌ Trước — vô tình bị quá hẹp
function setStatus(s: Status.Inactive) { ... }
// ✅ Sau — chấp nhận bất kỳ giá trị Status nào
function setStatus(s: Status) { ... }
const current = Status.Active;
setStatus(current); // OK
Kiểu quá hẹp này thường xuất hiện do autocomplete — bạn gõ Status., chọn Inactive, và TypeScript khóa kiểu literal lại trước khi bạn kịp nhận ra.
Cách Sửa 2: Dùng Union Các Thành Viên Cụ Thể
Cần chấp nhận một số thành viên nhưng không phải tất cả? Kiểu union giúp thể hiện rõ ràng ý định:
function handleResolved(s: Status.Active | Status.Inactive) {
console.log(s);
}
handleResolved(Status.Active); // ✅
handleResolved(Status.Inactive); // ✅
handleResolved(Status.Pending); // ❌ Bị từ chối đúng như mong đợi
Cách Sửa 3: Dùng Type Guard Trước Khi Gán
Khi bạn đã có một giá trị Status rộng nhưng cần truyền vào nơi yêu cầu một thành viên cụ thể, hãy dùng điều kiện kiểm tra trước. Phân tích luồng điều khiển của TypeScript sẽ tự xử lý phần còn lại:
function requireInactive(s: Status.Inactive) {
console.log('Inactive:', s);
}
function process(s: Status) {
if (s === Status.Inactive) {
requireInactive(s); // ✅ TypeScript thu hẹp s thành Status.Inactive tại đây
}
}
Bên trong khối if đó, TypeScript biết s chỉ có thể là Status.Inactive. Phép gán trở nên an toàn mà không cần ép kiểu.
Cách Sửa 4: Type Assertion (Phương Án Cuối Cùng)
Chỉ dùng cách này khi bạn chắc chắn giá trị runtime là đúng nhưng TypeScript không thể chứng minh được — ví dụ như sau khi lấy giá trị từ API bên ngoài:
const s = getStatusFromAPI() as Status.Inactive;
requireInactive(s); // ✅ — nhưng bạn đang bỏ qua kiểm tra kiểu
Nếu bạn thấy mình phải viết as SomeEnum.Member ở nhiều hơn một hoặc hai chỗ, thì thiết kế kiểu cần được xem xét lại — không phải thêm assertion.
Cách Sửa 5: Bỏ Enum, Dùng String Literal Union
Nhiều codebase TypeScript hiện đại bỏ qua enum hoàn toàn. String literal union đơn giản hơn, tạo ra thông báo lỗi rõ ràng hơn, và tránh được toàn bộ sự nhầm lẫn về kiểu literal:
// Thay vì dùng:
enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending',
}
// Hãy dùng:
type Status = 'active' | 'inactive' | 'pending';
function setStatus(s: Status) { ... }
setStatus('active'); // ✅
setStatus('inactive'); // ✅
setStatus('unknown'); // ❌ Bị từ chối đúng như mong đợi
Không có quirk của enum, không có runtime object, không phải lo về các giá trị được nhúng vào. Chỉ là các chuỗi với ràng buộc được trình biên dịch kiểm soát.
Cách Sửa 6: const Enum Cũng Gặp Vấn Đề Tương Tự
const enum nhúng các giá trị vào lúc biên dịch, nhưng quy tắc kiểu hoàn toàn giống nhau. Cách sửa tương tự — mở rộng kiểu hoặc dùng union:
const enum Direction {
Up,
Down,
}
// ❌ Quá hẹp
let d: Direction.Up = Direction.Down; // ts(2322)
// ✅ Đã mở rộng
let d: Direction = Direction.Down; // OK
Kiểm Tra Lại
Sau khi áp dụng cách sửa, hãy xác nhận lỗi đã được giải quyết:
- Đường gạch đỏ dưới phép gán sẽ biến mất ngay trong editor của bạn.
- Chạy kiểm tra kiểu toàn dự án để phát hiện những gì editor có thể bỏ sót:
npx tsc --noEmit
# Không có output = không có lỗi
- Nếu bạn dùng type guard (Cách sửa 3), hãy thêm một test nhanh kiểm tra cả nhánh khớp lẫn không khớp — xác nhận guard hoạt động đúng ở runtime, không chỉ ở compile time.
Tóm Tắt Nhanh
- Tham số quá hẹp → đổi kiểu từ
Status.XthànhStatus - Cần một tập con các thành viên → dùng
Status.X | Status.Y - Thu hẹp kiểu trong điều kiện → dùng
if (s === Status.X)trước khi truyền - Muốn tránh hoàn toàn sự phức tạp của enum → chuyển sang string literal union
Phòng Tránh
Nguyên nhân gốc rễ hầu như luôn giống nhau: một biến hoặc tham số vô tình được khai báo kiểu là một thành viên enum cụ thể (Status.Active) trong khi ý định là toàn bộ enum (Status). Autocomplete thường là thủ phạm — bạn điền vào một giá trị ban đầu và TypeScript khóa kiểu literal lại.
Ba thói quen giúp phòng tránh điều này:
- Viết annotation kiểu tường minh cho biến enum:
let s: Status = Status.Activethay vì để TypeScript tự suy raStatus.Activetừ vế phải. - Ưu tiên dùng string enum hoặc string literal union. Thông báo lỗi dễ đọc hơn và ít có các trường hợp đặc biệt gây bất ngờ hơn.
- Bật chế độ
stricttrongtsconfig.json. Nó phát hiện những điểm không khớp ngay tại nơi khai báo, trước khi chúng lan rộng qua ba tầng lời gọi hàm và trở nên khó hiểu.

