Strictly typed utilities for working with TypeScript enums.
- What is it?
- Other TypeScript Enum Projects
- Installation
- Usage Examples
- Requirements
- Limitations
- Known Issues
- General Concepts
- API Reference
ts-enum-util
provides utilities to improve the usefulness of enums. Examples include:
- Get a list of an enum's keys, values, or key/value pairs.
- Lookup values by key with run-time key validation and optional result defaulting.
- Reverse lookup of keys by value (for string enums too!) with run-time value validation and optional result defaulting.
- Run-time validation that a specified value or key is valid for a given enum, with compile-time type guards.
- Treat an enum like an Array.
- Treat an enum like a Map.
All of these utilities are very specifically typed for each enum via generics and type inference.
ts-string-visitor
[github, npm] - Generic TypeScript Visitor and Mapper for String Enums and String Literal Union Types
Install via NPM:
npm i -s ts-enum-util
Several small examples ts-enum-util
's capabilities to give you a quick overview of what it can do, as well as an organized "by example" reference.
Pay special attention to the comments indicating the compile-time type of various results. See Specific Typing for more about data types.
See API Reference for more details about method signatures and behaviors.
// import the $enum helper function
import {$enum} from "ts-enum-util";
// Example string enum
// (basic numeric enums also supported)
// (enums with a mix of numeric and string values also supported)
enum RGB {
R = "r",
G = "g",
B = "b"
}
Use the $enum function to get an EnumWrapper
instance for a particular enum.
Read about how EnumWrapper
instances are cached: Caching.
// type: EnumWrapper<string, RGB>
const wrappedRgb = $enum(RGB);
See also:
// Part of the Map-like interface implementation
// type: number
// value: 3
const size = $enum(RGB).size;
// Part of the Array-like interface implementation
// type: number
// value: 3
const length = $enum(RGB).length;
See also:
// type: ("R" | "G" | "B")[]
// value: ["R", "G", "B"]
const keys = $enum(RGB).getKeys();
// type: RGB[]
// value: ["r", "g", "b"]
const values = $enum(RGB).getValues();
// List of key/value pair tuples
// type: [("R" | "G" | "B"), RGB][]
// value: [["R", "r"], ["G", "g"], ["B", "b"]]
const entries = $enum(RGB).getEntries();
// type: RGB
// value: "g"
const value1 = $enum(RGB).getValueOrThrow("G");
// throws: Error("Unexpected value: blah. Expected one of: R,G,B")
const value2 = $enum(RGB).getValueOrThrow("blah");
// type: RGB | undefined
// value: undefined
const value3 = $enum(RGB).getValueOrDefault("blah");
// type: RGB
// value: "r"
const value4 = $enum(RGB).getValueOrDefault("blah", RGB.R);
// type: string
// value: "BLAH!"
const value5 = $enum(RGB).getValueOrDefault("blah", "BLAH!");
// type: ("R" | "G" | "B")
// value: "G"
const key1 = $enum(RGB).getKeyOrThrow("g");
// throws: Error("Unexpected value: blah. Expected one of: r,g,b")
const key2 = $enum(RGB).getKeyOrThrow("blah");
// type: ("R" | "G" | "B") | undefined
// value: undefined
const key3 = $enum(RGB).getKeyOrDefault("blah");
// type: ("R" | "G" | "B")
// value: "R"
const key4 = $enum(RGB).getKeyOrDefault("blah", "R");
// type: string
// value: "BLAH!"
const key4 = $enum(RGB).getKeyOrDefault("blah", "BLAH!");
// Some arbitrary string
declare const str: string;
// Returns `true` if 'str' is a valid key of RGB
if($enum(RGB).isKey(str)) {
// isKey() is a type guard
// type of 'str' in here is ("R" | "G" | "B")
}
// type: ("R" | "G" | "B")
// throws error if 'str' is not a valid key for RGB
const key1 = $enum(RGB).asKeyOrThrow(str);
// type: ("R" | "G" | "B") | undefined
// value is undefined if 'str' is not a valid key for RGB
const key2 = $enum(RGB).asKeyOrDefault(str);
// type: ("R" | "G" | "B")
// value is "G" if 'str' is not a valid key for RGB
const key3 = $enum(RGB).asKeyOrDefault(str, "G");
// Some arbitrary string
declare const str: string;
// Returns `true` if 'str' is a valid value of RGB
if($enum(RGB).isValue(str)) {
// isValue() is a type guard
// type of 'str' in here is RGB
}
// type: RGB
// throws error if 'str' is not a valid value for RGB
const value1 = $enum(RGB).asValueOrThrow(str);
// type: RGB | undefined
// value is undefined if 'str' is not a valid value for RGB
const value2 = $enum(RGB).asValueOrDefault(str);
// type: RGB | undefined
// value is RGB.G if 'str' is not a valid value for RGB
const value3 = $enum(RGB).asValueOrDefault(str, RGB.G);
See also:
const wrappedRgb = $enum(RGB);
// iterate all entries in the enum
wrappedRgb.forEach((value, key, wrappedEnum, index) => {
// type of value is RGB
// type of key is ("R" | "G" | "B")
// wrappedEnum is a reference to wrappedRgb
// index is based on sorted key order
});
// Convert all entries of the enum to an array of mapped values
// value: ["R: r", "G: g", "B: b"]
const mapped = wrappedRgb.map((value, key, wrappedEnum, index) => {
// type of value is RGB
// type of key is ("R" | "G" | "B")
// wrappedEnum is a reference to wrappedRgb
// index is based on sorted key order
return `${key}: ${value}`;
});
A wrapped enum can be treated like an array of enum "entry" tuples.
See also:
const wrappedRgb = $enum(RGB);
// type: number
// value: 3
const length = wrappedRgb.length;
// type: [("R" | "G" | "B"), RGB]
// value: ["G", "g"]
const entry = wrappedRgb[1];
A wrapped enum is similar to a read-only Map
of enum name -> enum value.
See also:
- Map-Like Interface (has explanation of why there's no
get()
orhas()
method) - Guaranteed Order of Iteration
const wrappedRgb = $enum(RGB);
// type: number
// value: 3
const size = wrappedRgb.size;
// EnumWrapper is directly iterable like a Map
for (const [key, value] of wrappedRgb) {
// type of key: ("R" | "G" | "B")
// type of value: RGB
}
for (const key of wrappedRgb.keys()) {
// type of key: ("R" | "G" | "B")
}
for (const value of wrappedRgb.values()) {
// type of value: RGB
}
wrappedRgb.forEach((value, key, wrappedEnum, index) => {
// type of value is RGB
// type of key is ("R" | "G" | "B")
// wrappedEnum is a reference to wrappedRgb
// index is based on sorted key order
// NOTE: index param is extra compared to Map's forEach
});
- ES6 Features: The following ES6 features are used by
ts-enum-util
, so they must exist (either natively or via polyfill) in the run-time environment:Map
WeakMap
Symbol.iterator
- Does not work with enums that are merged with a namespace containing values (variables, functions, etc.), or otherwise have any additional properties added to the enum's runtime object.
- Requires the
preserveConstEnums
TypeScript compiler option to work withconst enums
. - For certain
Iterable
features ofWrappedEnum
to work, you must either compile with a target ofes6
or higher, or enable thedownlevelIteration
compiler option.
Some WeakMap
polyfills store values directly on the "key" object (the run-time enum
object, in this case) as a non-enumerable "secret" (randomly generated) property. This allows for quick O(1) constant time lookups and garbage collection of the value along with the key object, but does add a property to the object. The WeakMap
secret property will NOT be iterated in for ... in
loops, and will NOT be included in the results of Object.keys()
, but it WILL be included in the result of Object.getOwnPropertyNames()
. It's hard to imagine this actually causing any problems, so I have decided to go ahead with relying on WeakMap
. If you run into a problem caused by this, please report an issue on github.
Read more about the use of WeakMap
for caching EnumWrapper
instances here: Caching
ts-enum-util
technically works with any "enum-like" object, which is any object whose property values are of type string
or number
.
The most obvious example is a TypeScript enum
. It can be a standard enum of numeric values, a string enum, or even an enum with a mix of numeric and string values.
The bulk of ts-enum-util
's functionality is implemented via an EnumWrapper
class, which is instantiated with a reference to an enum-like object and implements all the useful utility methods for that enum.
You likely won't ever directly reference the EnumWrapper
class because it's much more convenient to use the $enum function to obtain a reference to an EnumWrapper
instance.
The various methods of ts-enum-util
are generic and overloaded to ensure that params and results are as specifically-typed as possible. This is acheived through generics, type inference, and overloading of the $enum
function signature that is used to obtain an EnumWrapper
instance for a particular enum.
For example, when obtaining a key or keys from an EnumWrapper
, the data type will be a string literal union containing only the specific key names that exist in the enum.
This helps maximize the usefulness of ts-enum-util
by making it compatible with other strictly typed code related to enums.
A subset of EnumWrapper
's interface overlaps with much of the ES6 Map
interface. EnumWrapper
is similar to a read-only Map
of enum values, keyed by the enum names. The following Map-like features are implemented:
- size property.
- keys, values, and entries methods.
- forEach method.
- @@iterator method (
EnumWrapper
is iterable!).
NOTE: The Map
interface's has()
and get()
methods are intentionally NOT implemented in the interest of clarity and consistency of naming with respect to other EnumWrapper
-specific methods. The equivalent methods are isKey and getValueOrDefault (with second param omitted).
EnumWrapper
implements the ArrayLike
interface. It is usable as a readonly array of EnumWrapper.Entry. This allows you to pass an EnumWrapper
instance to any method that is designed read/iterate an array-like value, such as most of lodash's methods for collections and arrays.
ECMAScript does not guarantee a specific order when iterating properties/keys of objects. While many implementations do consistently iterate object properties/keys in the order in which they were added to the object, it is not safe to rely upon an assumption that all implementations will do the same.
EnumWrapper
sorts the keys of the enum and uses this sorted order to guarantee consistent ordering of all array/iterator results across all implementations. Just beware that the order may not be what you expect.
Example:
// enum defined with keys alphabetically out of order
enum ABC = {
B,
A,
C
}
// keys are sorted: ["A", "B", "C"]
const values = $enum(ABC).getKeys();
// values are ordered by sorted key order: [1, 0, 2]
const values = $enum(ABC).getValues();
EnumWrapper
instances are cached using an ES6 WeakMap
for quick subsequent retrieval via the $enum function. This allows you to easily access the EnumWrapper
functionality for a given enum via the $enum
function throughout your codebase without worrying about storing a reference to an EnumWrapper
that is accessible by all of the relevant code.
The use of the WeakMap
means that even if you use ts-enum-util
on temporary, dynamically-generated, enum-like objects, there will be no excessive cache bloat or memory leaks. A cached EnumWrapper
instance will be garbage collected when the enum-like object it is mapped to is garbage collected.
Although WeakMap
lookups can be extremely efficient (constant time lookups in typical implementations), beware that the ECMAScript specification only requires lookups to be "on average" less than O(n) linear time. As such, you should still excercise caution against needlessly obtaining cached references via $enum
when making heavy use of EnumWrapper
functionality. Consider storing the result of $enum()
in a local variable before making multiple calls to its methods, especially if the EnumWrapper
's features are used within a loop.
Despite the above warning, it is noteworthy that even the worst case implementation still produces extremely quick lookups for a relatively small number of items (like the number of enums that you are likely have in a project). For example, see this performance test of lookups into maps containing 500 entries, including a simple Map
polyfill implementation.
Read more about WeakMap
on the MDN website.
Also see the source code or the distributed index.d.ts
file for complete details of method signatures/overloads, detailed method/param documentation, etc.
See Usage Examples if you prefer a "by example" reference.
Throughout this reference, the following aliases for types will be used:
EnumLike
: An enum-like object type. See Enum-Like Object.KeyType
: The type of the enum's keys. This is usually a string literal union type of the enum's names, but may also simply bestring
if anEnumWrapper
was created for an object whose possible property names are not known at compile time.EnumType
: The specific enum type of the enum values. This is usually the enum type itself, but may also simply be the same asValueType
(see below) if aEnumWrapper
was created for an object that is not actually an enum, but is only "enum-like".ValueType
: The widened type of the enum's values. Will benumber
,string
, ornumber | string
, depending on whether the wrapped enum-like object contains only number, only string, or both number and string values.
This is where it all begins. This method returns an EnumWrapper instance that provides useful utilities for enumObj
.
See Caching for more about caching of EnumWrapper
instances.
function $enum(
enumObj: EnumLike
): EnumWrapper
enumObj
- An enum or "enum-like" object.
This is the class that implements all the enum utilities. It's a generic class that requires an overloaded helper function to properly instantiate, so the constructor is private. Use $enum() to get/create an instance of EnumWrapper
.
class EnumWrapper
A generic type alias for a tuple containing a key and value pair, representing a complete "entry" in the enum. The tuple is defined as Readonly
to prevent accidental corruption of the EnumWrapper
instance's data.
type EnumWrapper.Entry = Readonly<[KeyType, EnumType]>
A generic type alias for a function signature to be used in iteration methods. This is compliant with the signature of an iteratee for a Map<KeyType, EnumType>.forEach()
method, but also has an additional index
param at the end of the parameter list.
See Guaranteed Order of Iteration for details about the ordering.
type EnumWrapper.Iteratee<R> = (
value: EnumType,
key: KeyType,
enumWrapper: EnumWrapper,
index: number
) => R
R
- Generic type param for the return type of the function. The signifigance of the return type/value depends on teh context in which the iteratee is being used.value
- The value of the enum entry.key
- The key of the enum entry.enumWrapper
- A reference to theEnumWrapper
instance that is being iterated.index
- The index of the enum entry
See also: Array-Like Interface
A read-only property containing the number of entries in the enum.
readonly EnumWrapper.prototype.length: number
The index signature is implemented on EnumWrapper
to allow you to access [key, value]
tuples by index like an array. The values accessed by indexing are readonly.
See Guaranteed Order of Iteration for details about the ordering.
readonly EnumWrapper.prototype.[index: number]: EnumWrapper.Entry
See also: Map-Like Interface
A read-only property containing the number of entries in the enum.
readonly EnumWrapper.prototype.size: number
Returns an Iterator
that will iterate all keys of the enum.
See Guaranteed Order of Iteration for details about the ordering.
EnumWrapper.prototype.keys(): IterableIterator<KeyType>
Returns an Iterator
that will iterate all values of the enum.
See Guaranteed Order of Iteration for details about the ordering.
EnumWrapper.prototype.values(): IterableIterator<EnumType>
Returns an Iterator
that will iterate all [key, value] pairs of the enum.
See Guaranteed Order of Iteration for details about the ordering.
EnumWrapper.prototype.entries(): IterableIterator<EnumWrapper.Entry>
Same as EnumWrapper.prototype.entries.
Allows an EnumWrapper
to be directly iterated as a collection of [key, value]
tuples.
EnumWrapper.prototype.@@iterator(): IterableIterator<EnumWrapper.Entry>
Iterates every entry in the enum and calls the provided iteratee
function.
See Guaranteed Order of Iteration for details about the ordering.
EnumWrapper.prototype.forEach(
iteratee: EnumWrapper.Iteratee,
context?: any
): void
iteratee
: See EnumWrapper.Iteratee. The return value of this function is ignored.context
: If provided, then the value will be used as thethis
context when executingiteratee
.
See EnumWrapper.prototype.forEach in the Map-Like Interface section.
Builds and returns a new array containing the results of calling the provided iteratee
function on every entry in the enum.
See Guaranteed Order of Iteration for details about the ordering.
EnumWrapper.prototype.map<R>(
iteratee: EnumWrapper.Iteratee,
context?: any
): R[]
R
: Generic type param that indicates the type of entries in the resulting array. If not specified, then it will be inferred from the return type ofiteratee
.iteratee
: See EnumWrapper.Iteratee.context
: If provided, then the value will be used as thethis
context when executingiteratee
.
Returns an array of all keys in the enum.
See Guaranteed Order of Iteration for details about the ordering.
EnumWrapper.prototype.getKeys(): KeyType[]
Returns an array of all values in the enum. If the enum contains any duplicate values, then so will the returned array.
See Guaranteed Order of Iteration for details about the ordering.
EnumWrapper.prototype.getValues(): EnumType[]
Returns a list of [key, value]
tuples representing all entries in the enum.
See Guaranteed Order of Iteration for details about the ordering.
EnumWrapper.prototype.getEntries(): EnumWrapper.Entry[]
Returns `true` if the provided `key` is a valid key for the enum.
Also acts as a type guard to tell the compiler that the provided `key` is the more specific `KeyType` type.
EnumWrapper.prototype.isKey(
key: string | null | undefined
): key is KeyType
If the provided key
is a valid key for the enum, then the key
is returned, but cast to the more specific KeyType
type.
If the provided key
is NOT valid, then an Error
is thrown.
EnumWrapper.prototype.asKeyOrThrow(
key: string | null | undefined
): KeyType
If the provided key
is a valid key for the enum, then the key
is returned, but cast to the more specific KeyType
type.
If the provided key
is NOT valid, then defaultKey
is returned.
This method is overloaded so that its return type is as specific as possible, depending on the type of the defaultKey
param.
EnumWrapper.prototype.asKeyOrDefault(
key: string | null | undefined,
defaultKey?: KeyType | string
): KeyType | string | undefined
Returns true
if the provided value
is a valid value for the enum.
Also acts as a type guard to tell the compiler that the provided value
is the more specific EnumType
type.
EnumWrapper.prototype.isValue(
value: ValueType | null | undefined
): key is EnumType
If the provided value
is a valid value for the enum, then the value
is returned, but cast to the more specific EnumType
type.
If the provided value
is NOT valid, then an Error
is thrown.
EnumWrapper.prototype.asValueOrThrow(
value: ValueType | null | undefined
): EnumType
If the provided value
is a valid value for the enum, then the value
is returned, but cast to the more specific EnumType
type.
If the provided value
is NOT valid, then defaultValue
is returned.
This method is overloaded so that its return type is as specific as possible, depending on the type of the defaultValue
param.
EnumWrapper.prototype.asValueOrDefault(
value: ValueType | null | undefined,
defaultValue?: EnumType | ValueType
): EnumType | ValueType | undefined
Performs a reverse lookup to get the key that corresponds to the provided value
.
If the enum has duplicate values matching the provided value
, then the key for the last duplicate entry (in order specified by the Guaranteed Order of Iteration section) is returned.
If the provided value
is NOT valid, then an Error
is thrown.
EnumWrapper.prototype.getKeyOrThrow(
value: ValueType | null | undefined
): KeyType
Performs a reverse lookup to get the key that corresponds to the provided value
.
If the enum has duplicate values matching the provided value
, then the key for the last duplicate entry (in order specified by the Guaranteed Order of Iteration section) is returned.
If the provided value
is NOT valid, then defaultKey
is returned.
This method is overloaded so that its return type is as specific as possible, depending on the type of the defaultKey
param.
EnumWrapper.prototype.getKeyOrDefault(
value: ValueType | null | undefined,
defaultKey?: KeyType | string
): KeyType | string | undefined
Returns the value corresponding to the provided key
.
If the provided key
is NOT valid, then an Error
is thrown.
EnumWrapper.prototype.getValueOrThrow(
key: string | null | undefined
): EnumType
Returns the value corresponding to the provided key
.
If the provided key
is NOT valid, then defaultValue
is returned.
This method is overloaded so that its return type is as specific as possible, depending on the type of the defaultValue
param.
EnumWrapper.prototype.getValueOrDefault(
key: string | null | undefined,
defaultValue?: EnumType | ValueType
): EnumType | ValueType | undefined