better-enums
v0.2.2
Published
Better enums for TypeScript
Downloads
234
Readme
better-enums
Better enums for TypeScript.
The better-enums
library provides a simple utility for creating an improved version of TypeScript enums.
Full documentation is hosted here.
Motivation
Enums vs unions
Many in the TypeScript community consider using TypeScript's built-in enum
keyword a bad practice (Anders Hejlsberg himself has stated he wouldn't have put them into the language in retrospect). The recommendation is to use union types instead, which may be inferred from arrays or objects when some runtime representation is also needed (e.g. for iteration). For more information on this topic, see:
- Enums considered harmful by Matt Pocock,
- Let's Talk About TypeScript's Worst Feature by Theo Browne,
- Should You Use Enums or Union Types in Typescript? by Matthieu Gicquel.
Best of both worlds
This library provides a custom enum implementation which attempts to avoid the pitfalls of TS enums, while also enhancing unions with runtime features.
These "better enums" are created either from an array of values (a "simple enum") or an object mapping keys to values (a "labeled enum"), with a union type then being automatically inferred from these values. This pattern is the commonly recommended alternative to the enum
keyword. Using this library has the advantage of encapsulating some of the necessary TypeScript magic that can be daunting for less experienced TypeScript programmers.
The labeled enums offer good compatibilty with built-in enums. Since they support the same dot syntax and can even be created from an existing built-in enum, migrating away from the enum
keyword should be fairly straightforward for existing projects.
The main advantages of using better-enums
over built-in enums and/or unions are:
- 😌 simplifies the recommended "inferred union" pattern,
- ⛔ no built-in enum weirdness (e.g. no nominal typing, no mixed-in reverse mapping),
- 🛡️ provides convenient type guards for CFA-compatible runtime checks (i.e. automatic type narrowing),
- 📜 provides array of all values (labeled enums also provide arrays of keys or key-value pairs),
- 🏭 includes helper functions for composing new enums from existing ones (by extending values or excluding from them),
- 📈 can easily convert built-in enums,
- 🌑 support for dot syntax (optional),
- 💙 excellent type-safety in general.
Setup
Install the better-enums
package using your favourite package manager:
npm install better-enums
yarn add better-enums
pnpm add better-enums
Usage
Creating enums
Simple enums
Import the Enum
callable object to create a simple enum and infer its union type with the InferValue
utility type:
import { Enum, type InferValue } from 'better-enums';
const ROLES = Enum(['viewer', 'editor', 'admin']);
type Role = InferValue<typeof ROLES>;
Then you can use the inferred type in your type definitions:
type User = {
email: string;
role: Role; // role is 'viewer' | 'editor' | 'admin'
};
If you prefer an enum-style syntax for accessing values, you can use the .accessor
property (keys match values exactly):
const ROLES = Enum(['Viewer', 'Editor', 'Admin']);
type Role = InferValue<typeof ROLES>;
const Role = ROLES.accessor;
// ...
let role: Role;
// these are equivalent
role = Role.Admin;
role = 'Admin';
The enum object enables you to use runtime features:
list all values with
.values()
:ROLES.values().forEach(role => { console.log(role); });
check value with
.hasValue(x)
(returnstrue
/false
):function f(value: string | undefined) { // value is string | undefined if (ROLES.hasValue(value)) { // value is Role } }
check value with
.assertValue(x)
(throwsRangeError
if invalid):function f(value: string | undefined) { try { // value is string | undefined ROLES.assertValue(value); // value is Role } catch (err: unknown) { if (err instanceof RangeError) { // 'Enum value out of range (received undefined, expected one of: "user", "admin", "superadmin")' console.warn(err.message); } } }
Labeled enums
If you prefer to use something more similar to classic enums, you can provide an object instead of an array when calling Enum
:
const ROLES = Enum({
Viewer: 'viewer',
Editor: 'editor',
Admin: 'admin',
});
type Role = InferValue<typeof ROLES>;
const Role = ROLES.accessor;
Then you can access enum values either directly or via their key:
function createUser(email: string, role: Role) {}
// these are equivalent
createUser('[email protected]', Role.Admin);
createUser('[email protected]', 'admin');
Labeled enums support all the methods of simple enums (e.g. .values()
or .hasValue(x)
), as well as additional methods:
- list all keys with
.keys()
, - check key with
.hasKey(x)
or.assertKey(x)
, - list all key-value pairs with
.entries()
, - check key-value pair with
.hasEntry([x, y])
or.assertEntry([x, y])
, - get key for given value with
.keyOf(x)
.
Composing enums
In addition to creating brand new enums, you can easily derive new enums from existing ones.
Adding values (Enum.extend
)
To add values to a simple enum, pass in an array of values:
const ROLES = Enum(['viewer', 'editor', 'admin']);
const ENHANCED_ROLES = Enum.extend(ROLES, ['superadmin']);
// equivalent to: Enum(['viewer', 'editor', 'admin', 'superadmin'])
To add values to a labeled enum, pass in an object:
const ROLES = Enum({
Viewer: 'viewer',
Editor: 'editor',
Admin: 'admin',
});
const ENHANCED_ROLES = Enum.extend(ROLES, {
SuperAdmin: 'superadmin',
});
/* equivalent to:
const ENHANCED_ROLES = Enum({
Viewer: 'viewer',
Editor: 'editor',
Admin: 'admin',
SuperAdmin: 'superadmin',
});
*/
If you pass in an array of values for a labeled enum, the result will be a simple enum:
const ROLES = Enum({
Viewer: 'viewer',
Editor: 'editor',
Admin: 'admin',
});
const ENHANCED_ROLES = Enum.extend(ROLES, ['superadmin']);
// equivalent to: Enum(['viewer', 'editor', 'admin', 'superadmin'])
Removing values (Enum.exclude
)
To remove values from a simple enum, pass in an array of values:
const ROLES = Enum(['viewer', 'editor', 'admin']);
const RESTRICTED_ROLES = Enum.exclude(ROLES, ['admin']);
// equivalent to: Enum(['viewer', 'editor'])
To remove values from a labeled enum, you have two alternatives:
pass in an array of keys:
const ROLES = Enum({ Viewer: 'viewer', Editor: 'editor', Admin: 'admin', }); const RESTRICTED_ROLES = Enum.exclude(ROLES, ['Admin']); /* equivalent to: const RESTRICTED_ROLES = Enum({ Viewer: 'viewer', Editor: 'editor', }); */
pass in an array of values:
const ROLES = Enum({ Viewer: 'viewer', Editor: 'editor', Admin: 'admin', }); const RESTRICTED_ROLES = Enum.exclude(ROLES, ['admin']); /* equivalent to: const RESTRICTED_ROLES = Enum({ Viewer: 'viewer', Editor: 'editor', }); */
Converting enums
If you're stuck with some built-in TypeScript enum
s in your project (e.g. from some code generator), you can easily upgrade them to better enums. 🙂
Convert a built-in string enum
enum Role = {
Viewer = 'viewer',
Editor = 'editor',
Admin = 'admin',
}
const ROLES = Enum(Role);
/* equivalent to:
const ROLES = Enum({
Viewer: 'viewer',
Editor: 'editor',
Admin: 'admin',
});
*/
Convert a built-in number enum
enum Role = {
Viewer,
Editor,
Admin,
}
const ROLES = Enum(Role);
/* equivalent to:
const ROLES = Enum({
Viewer: 0,
Editor: 1,
Admin: 2,
});
*/
For numeric enums, the Enum
function takes care of excluding the reverse mapping in the underlying runtime representation TypeScript creates. E.g. .keys()
will only include keys and .values()
will include values, unlike if you called Object.keys
or Object.values
on the original enum
.
Contributing
- install dependencies using
npm install
, - run tests (written with Jest and TSD) using
npm test
, - build library using
npm run build
, - generate documentation (with TypeDoc) using
npm run docs
.