Skip to content

Simple, typesafe enums in TypeScript, fully compatible with standard JavaScript

License

Notifications You must be signed in to change notification settings

ncoderz/superenum

Repository files navigation

@ncoderz/superenum

Build & Test

Simple, typesafe enums in TypeScript, fully compatible with standard JavaScript.

Why @ncoderz/superenum?

The standard TypeScript enum implementation is over-complicated, missing basic features, and is not easily compatible with standard JavaScript enums.

@ncoderz/superenum provides an alternative that is:

  • an extension of standard JavaScript object enums
  • simple to use
  • type-safe
  • supports IDE autocompletion
  • iteration order can be guaranteed
  • is interoperable with standard JavaScript enums
  • works in node or the browser
  • has a very small code footprint (< 1kB minified + gzipped)

Additionally, the library is committed to:

  • an API that will always remain backwards compatible
  • no dependencies
  • permissive license (BSD-2-Clause)
  • fixing bugs

Installation

yarn add @ncoderz/superenum
npm install --save @ncoderz/superenum

Importing

@ncoderz/superenum can be used via ES Module import, CommonJS, or as a script import directly in the browser.

import { superenum, EnumType } from '@ncoderz/superenum';
const { superenum, EnumType } = require('@ncoderz/superenum');
<script src="https://cdn.jsdelivr.net/npm/@ncoderz/superenum/dist/browser/superenum.min.js"></script>
<script
  const { superenum } = window.superenum;

</script>

Enum Declaration

Enums are declared as JavaScript objects wrapped with the superenum.fromObject(enumObject) function.

As long as the keys and values of the object are constants, or TypeScript can infer a constant value, then the enum will be typesafe and IDE auto-completion will work.

Enums can also be declared as JavaScript arrays using superenum.fromArray(enumArray). These will be expanded to objects with the same keys as values, and they will behave as if they had be declared as such.

// String enum
const MyEnum = superenum({ // ALIAS: superenum.fromObject({
  node: 'node',
  chrome: 'chrome',
  safari: 'safari',
});
type MyEnumType = EnumType<typeof MyEnum>; // Optional type declaration

// Equivalent declaration using an array
const MyEnumFromArray = superenum.fromArray([
  'node',
  'chrome',
  'safari',
]);
type MyEnumFromArrayType = EnumType<typeof MyEnumFromArray>  // Optional type declaration

// Numeric enum
const MyNumericEnum = superenum({
  node: 0,
  chrome: 1,
  safari: 2,
});
type MyNumericEnumType = EnumType<typeof MyNumericEnum>  // Optional type declaration

// Mixed enum with different values as string keys
const MyMixedEnum = superenum({
  node: 'NodeJS',
  chrome: 1,
  safari: 'MacOSSafari',
});
type MyMixedEnumType = EnumType<typeof MyMixedEnum> // Optional type declaration

Basic Usage

const MyEnum = superenum({
  node: 'node',
  chrome: 'chrome',
  safari: 'MacOSSafari',
});
type MyEnumType = EnumType<typeof MyEnum>;

// End enum declaration

// Basic variable and comparison
const value: MyEnumType = MyEnum.node;

console.assert(value === MyEnum.node)    // true
console.assert(value === 'node')         // true
console.assert(value === MyEnum.chrome)  // false
console.assert(value === 'firefox')      // false

// Switch statements
switch (value) {
  case MyEnum.node:        // true
    break;
  case MyEnum.chrome:      // false
    break;
  case MyEnum.safari:      // false
    break;
  default:
}

switch (value) {
  case 'node':             // true
    break;
  case 'chrome':           // false
    break;
  case 'MacOSSafari':      // false
    break;
  case 'safari':           // TS compiler error
    break;
  case 'something else':   // TS compiler error
    break;
  default:
}

Validation

Validation is a common use case when reading data from an API, file or database.

@ncoderz/superenum makes this easy with <enum>.fromValue() which returns a typed enum or undefined if the data does not match an enum value.

const MyEnum = superenum.fromArray([
  'node',
  'chrome',
  'safari',
]);

// End enum declaration

// Input values could come from external data / API
const valueNode = MyEnum.fromValue('node');         // MyEnum.node / 'node'
const valueChrome = MyEnum.fromValue('chrome');     // MyEnum.chrome / 'chrome'
const invalid = MyEnum.fromValue('surfari');        // undefined
const invalid2 = MyEnum.fromValue(undefined);       // undefined

// Validate with default of MyEnum.node
const validOrDefault = MyEnum.fromValue('invalid') ?? MyEnum.node;  // MyEnum.node / 'node'

Iteration / forEach / map / reduce

It is easy to iterate the enum values, keys or entries.

The <enum> itself is iterable and will iterate the values in the defined key order.

The <enum>.values(), <enum>.keys(), <enum>.entries() functions return arrays in the defined key order which can be iterated, mapped, reduced, etc.

const MyNumericEnum = superenum({
  node: 0,
  chrome: 1,
  safari: 2,
});

// End enum declaration

// Iterate enum values
for (const value of MyNumericEnum) {
  console.log(value)
}
// 0
// 1
// 2

for (const value of MyNumericEnum.values()) {
  console.log(value)
}
// 0
// 1
// 2

MyNumericEnum.values().forEach((value) => {
  console.log(value)
});
// 0
// 1
// 2

MyNumericEnum.values().map((value) => {
  console.log(value + 1)
});
// 1
// 2
// 3

// Iterate enum keys (forEach, map, etc also work on keys())
for (const value of MyNumericEnum.keys()) {
  console.log(value)
}
// node
// chrome
// safari

// Iterate enum entries (forEach, map, etc also work on entries())
for (const value of MyNumericEnum.entries()) {
  console.log(value)
}
// [ 'node', 0 ]
// [ 'chrome': 1 ]
// [ 'safari': 2 ]

The order of iteration is guaranteed unless an enum key can be converted to an integer. In this case it can be guaranteed by using superenum.fromArray() to initialise the enum, or by setting EnumOptions.iterationKeys to represent the desired iteration order.

// Ensure iteration order when using integer keys mixed with strings and creating
// the object with `superenum() / superenum.fromObject()
//
// In this case if iterationKeys was not used, the iteration order would be defined
// by the JavaScript engine and would probably be:
// 1, 2, node, safari

const MyMixedEnum = superenum({
  node: 0,
  1: 1,
  safari: 2,
  '2': 'two'
}, {
  iterationKeys: ['node', 1, 'safari', '2']
});

// Iterate enum keys
for (const value of MyMixedEnum.keys()) {
  console.log(value)
}
// node
// 1
// safari
// 2

Enum value from enum key

There are use-cases where you might want to get an enum value from the enum key. An example might be where the key is stored in a configuration file, and matched to a value in code.

This is possible with <enum>.fromKey()

const MyEnum = superenum({
  node: 0,
  chrome: 1,
  safari: 2,
});

// End enum declaration

const nodeValue = MyEnum.fromKey('node');       // MyEnum.node / 0
const invalidValue = MyEnum.fromKey('NoDe');    // undefined
const nodeValue2 = MyEnum.fromKey('NoDe', {
  ignoreCase: true,
});                                             // MyEnum.node / 0

Enum key from enum value

There are use-cases where you might want to get an enum key from the enum value. An example might be for logging purposes.

This is possible with <enum>.keyFromValue()

const MyEnum = superenum({
  node: 'NodeJS',
  chrome: 'GoogleChrome',
  safari: 'MacOSSafari',
});

// End enum declaration

const nodeKey = MyEnum.keyFromValue('NodeJS');        // 'node'
const nodeKey2 = MyEnum.keyFromValue(MyEnum.node);  // 'node'
const invalid = MyEnum.keyFromValue('node');          // undefined

Metadata

Want to associate some data with a particular enum value? An example might be to store a set of description strings against a set of keys... the possibilities are endless.

This is possible with <enum>.setMetadata() and <enum>.getMetadata()

const MyEnum = superenum.fromArray([
  'node',
  'chrome',
  'safari',
]);

MyEnum.setMetadata(MyEnum.node, 'Node.js is an open-source, cross-platform...');
MyEnum.setMetadata(MyEnum.chrome, 'Chrome is faster than fast – it’s engine...');
MyEnum.setMetadata(MyEnum.safari, 'Safari is the best way to experience the...');

// End enum declaration

// Input value could come from external data / API
const valueNode = MyEnum.fromValue('node');   // MyEnum.node / 'node'
const desc = MyEnum.getMetadata(valueNode);   // 'Node.js is an open-source, cross-platform...'

API

API Documentation

Feature Comparison

Feature @ncoderz/enum (numeric and string) TypeScript enum (numeric) TypeScript enum (string)
Compatible with simple JS enums Yes X X
Mixed numeric and string keys Yes X X
Compare value value === Enum.key Enum[value] === Enum[Enum.key] value === Enum.key
Validate external data to valid typed enum const val: EnumType = Enum.fromValue(value) const val: Enum = ((Enum[value] === Enum[Enum.key]) ? value : undefined) as Enum const val: Enum = ((value === Enum.key) ? value : undefined) as Enum
Value from key const val: EnumType = Enum.fromKey(key) Enum[key] - only if key is const; not very useful Object.entries(Enum).reduce((acc, [k,v]) => { if (acc) return acc; if (k === key) return v; }, '') - not typed (is string)
Value from key safe? Safe No, will return key for invalid input which happens to be a valid value Safe, but see above!
Key from value Enum.keyFromValue(value) Enum[value] Object.entries(Enum).reduce((acc, [k,v]) => { if (acc) return acc; if (v === value) return k; }, '')
Key from value safe? Safe No, will return value for invalid input which happens to be a valid key Safe, but see above!
Iterate values? Enum.values() Object.values(Enum).filter((v) => isNaN(Number(v)) Object.values(Enum)
Iterate keys? Enum.keys() Object.keys(Enum).filter((k) => !isNaN(Number(k)) Object.keys(Enum)
Iterate entries? Enum.entries() Object.entries(Enum).filter(([k,v]) => isNaN(Number(k))) Object.entries(Enum)
Associate metadata with an enum value Yes X X
Declare just first number value and increment X Yes N/A
Bloat code? < 1kB Built-in Built-in
Fast? Yes Workarounds involve looping Workarounds involve looping

Limitations

Enums may not use the keys:

fromKey, fromValue, keyFromValue, setMetadata, getMetadata, keys, values, entries

as these clash with the extension functions.

If such a key is used, it will be overwitten with the extension function.

License

This open source software is licenced under the BSD-2-Clause licence.

Alternatives