A runtime-safe alternative to TypeScript enums that maintains type safety and ergonomics.
import { Enum } from 'unique-enum'; // ESM
const { Enum } = require('unique-enum'); // CommonJS
const Direction = Enum(
'North',
'East',
'South',
'West'
);You can install unique-enum from npm:
npm install unique-enumAn enum can be declared as in the first example. The Enum function returns a class with its members as its only possible instances. Its members will autocomplete like normal enum variants (eg. typing Direction. will show North, East etc. after declaration.)
Each variant's string representation can be accessed using the variant property or by calling toString:
console.log(Direction.North.variant) // prints: 'North'Enums also have convenient string representations and will work normally with Object.keys, iterators etc:
console.log(Direction.toString());
// prints: Enum { North, East, South, West }
console.log(Object.keys(Direction));
// prints: [ 'North', 'East', 'South', 'West' ]
for(let direction of Direction) { /* iterates over the variants */ }A unique enum guarantees the following invariants throughout the program:
- Each variant will only be equal to itself and no other object or primitive.
- Variants are the sole objects for which
instanceof enumwill be true. - After declaration, no new variants can be constructed or assigned, and variants cannot be changed or destroyed.
new Direction('northwest'); // Error
Direction.NorthWest = {}; // Error
Direction.North = 0; // Error
Direction.North.variant = 'South' // Error
delete Direction.North; // Error
function is_north(d) {
return (
d instanceof Direction // Only true for the 4 Direction instances
&& d == Direction.North // Only true for Direction.North
);
}These invariants make unique enums more resilient at runtime and when used from vanilla JavaScript.
Since unique enums maintain type information at runtime, inspecting them makes debugging easier compared to TypeScript's implementation.
console.log(Direction.North) // prints: Value { variant: 'North' }If you want to use variants of a specific enum as an object property or function argument, you can use the provided Variant type.
import { Variant } from 'unique-enum';
function is_north(d: Variant<typeof Direction>): d is typeof Direction.North {
return d == Direction.North;
}
is_north(Direction.North); // true
is_north(Direction.South); // false
isNorth({ variant: 'North' }); // ErrorUnfortunately, unlike TypeScript enums, unique enums cannot autofill switch statements at runtime, and hacks like a default arm that returns never will not work to check for exhaustiveness. This is because the type checker is unable to check for exhaustiveness of types that are not unions of literal types.
TypeScript's type system has a limitation when dealing with unique enum variants: For two enums E1 and E2, a variant of E1 is considered compatible with a variant of E2 if their names are the same, and the variant names of E1 form a subset of those in E2.
const SoftwareVersion = Enum("V1", "V4", "V6");
const IP = Enum("V4", "V6");
let incompatible: typeof SoftwareVersion.V4 = IP.V4; // Assignment is allowed despite the types being incompatibleNote that the assignment will instead fail if IP contains any variant name that is not also a variant name of SoftwareVersion, so this should not be a problem in practice.
This flaw is due to the type system's inability to distinguish between different instances of classes returned from a function even if they have non-overlapping private members, an issue which is considered a design limitation.
However, note that TypeScript enums are themselves not fully type safe. While they do not suffer from this specific flaw, enum variant types are considered equivalent to their literal types, so the following is valid code:
enum IP { V4, V6 }
let literal: IP.V4 = 0; // No error even though 0 is not an IP