Skip to content

Runtime-safe alternative to typescript enums that maintains type safety and ergonomics

p2js/unique-enum

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

unique-enum

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'
);

Installation

You can install unique-enum from npm:

npm install unique-enum

Usage

An 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 */ }

Type Invariants

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 enum will 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.

Debugging enum values

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' }

Unique Enums + TypeScript

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' }); // Error

Unfortunately, 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.

Limitation of TypeScript's Type System

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 incompatible

Note 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

About

Runtime-safe alternative to typescript enums that maintains type safety and ergonomics

Topics

Resources

Stars

Watchers

Forks