UNPKG

@northscaler/better-enum

Version:

Better enumeration support for TypeScript than its `enum` keyword. This class is modeled after [Java's enumeration pattern](https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html), where enums are instances of classes. This library provides a base

216 lines (192 loc) 8.07 kB
import isValidJavaScriptIdentifier from './is-valid-javascript-identifier' import { DuplicateEnumerationDeclarationError, IllegallyExtendedEnumerationError, InvalidEnumerationNameError, InvalidEnumerationOrdinalError, UnidentifiableEnumerationValueError, UnknownEnumerationClassError, } from './errors' type EnumerationsByName = { [name: string]: any } type EnumerationsByOrdinal = { [ordinal: number]: any } // key type is a class function; that is, given `class Foo {}`, then symbol `Foo` const enums = new Map< // eslint-disable-next-line @typescript-eslint/ban-types Function, { byName: EnumerationsByName; byOrdinal: EnumerationsByOrdinal } >() /** * Base enumeration class for the pattern where enumerated values are represented by instances of classes rather than by scalar string or numeric values alone. * * Every subclass `T` has the following requirements. * * * `T` must not be extended. * * * `T` must define a `private` constructor whose first two arguments are `name: string, ordinal: number`, * * where `name` is a valid JavaScript identifier that is the same as the instance's static identifier on `T`, and * * where `ordinal` is an integral value that is unique amongst all instances. * * * `T` must define a `static` method `of(it: T | string | number): T` that returns an instance of `T` whose `name` or `ordinal` is equal to the given value, or that is identical to the instance of `T` given, and must throw {@link UnidentifiableEnumerationValueError} otherwise. * * > NOTE: {@link Enumeration} provides a convenient function that subclasses can delegate to in order to implement the required `of` method above. * > Use `import { _of } from '@northscaler/better-enum' and delegate the subclass's `of()` method to it. * * * `T` must define a `static` method `values(): T[]` that returns all instances of `T`. * No ordering is prescribed. * However, you can use one of the convenient sorting functions provided by this class, {@link sortByName} or {@link sortByOrdinal}, to sort the instances, or write your own. * * > NOTE: {@link Enumeration} provides a convenient function that subclasses can delegate to in order to implement the required `values` method above. * > Use `import { _values } from '@northscaler/better-enum' and delegate the subclass's `values()` method to it. */ export class Enumeration<T extends Enumeration<T>> { /** * Constructs an enumerated value. * @param _name The symbolic name of this instance. * It must be a valid JavaScript symbol and must be unique amongst all instances of this class's type. * @param _ordinal The numeric ordinal of this instance. * It must be an integer value and must be unique amongst instances of this class's type. * @param _class The class function of the subclass calling this constructor. * For example, given `class Bool extends Enumeration<Bool> { ... }`, the reference to the class's function is `Bool`. * @throws InvalidEnumerationNameError If the `_name` parameter is not a valid JavaScript identifier. * @throws InvalidEnumerationOrdinalError If the `_ordinal` parameter is not an integer. * @throws DuplicateEnumerationDeclarationError If there is already a value with the given `_name` or `_ordinal` amongst all instances of this class's type. * @protected */ protected constructor( protected _name: string, protected _ordinal: number, // eslint-disable-next-line @typescript-eslint/ban-types protected _class: Function ) { if ( Object.getPrototypeOf(Object.getPrototypeOf(this)).constructor.name !== Enumeration.name ) { throw new IllegallyExtendedEnumerationError({ context: { this_: this } }) } if (!isValidJavaScriptIdentifier(_name)) { throw new InvalidEnumerationNameError({ context: { name: _name } }) } const n = parseInt(String(_ordinal)) if (isNaN(n) || n !== _ordinal) { throw new InvalidEnumerationOrdinalError({ context: { ordinal: _ordinal }, }) } let class_ = enums.get(this._class) if (!class_) { enums.set(this._class, (class_ = { byName: {}, byOrdinal: {} })) } if (class_!.byName[_name] || class_!.byOrdinal[_ordinal]) { throw new DuplicateEnumerationDeclarationError({ context: { name: _name, ordinal: _ordinal }, }) } class_!.byName[_name] = class_!.byOrdinal[_ordinal] = (this as unknown) as T } /** * The symbolic name of this enumerated value. */ name() { return this._name } /** * The integer ordinal of this enumerated value. */ ordinal() { return this._ordinal } /** * Returns whether the given object has the same name, ordinal & constructor name as this object. */ equals(that: T) { return ( this.constructor.name === that.constructor.name && this.name() === that.name() && this.ordinal() === that.ordinal() ) } /** * Returns the symbolic name of this enumerated value. * @param fullyQualified Whether to return the return value of {@link toFullyQualifiedString} or just the symbolic name; defaults to `false`. */ toString(fullyQualified = false) { return fullyQualified ? this.toFullyQualifiedString() : this._name } /** * Returns a colon-separated string of this class's name, its symbolic name, and its ordinal (like `Bool:TRUE:1`). */ toFullyQualifiedString() { return `${this.constructor.name}:${this._name}:${this._ordinal}` } /** * Default `toJSON()` protocol method. * Returns the symbolic name of this enumerated value. * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior * * To implement a JSON [`reviver`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter), use the static `of` method defined on your class. */ toJSON() { return this.name() } } /** * A function that attempts to locate an enumerated value given its name or ordinal. * If given an argument that is `typeof object`, it must be an enumerated value, which is simply returned. * Any value that is not among the symbolic names, ordinals, or instances of the enumeration class will cause this method to throw an `UnidentifiableEnumerationValueError`. * * This function is only intended to be used by subclasses of {@link Enumeration}. * * @param it The value being used to identify the enumerated value. * @param clazz A reference to the enumeration class (if `class Foo ... {}`, then the reference `Foo` should be given). */ export function _of<T extends Enumeration<T>>( it: T | string | number, // eslint-disable-next-line @typescript-eslint/ban-types clazz: Function ): T { let e: T const type = typeof it const f = enums.get(clazz) if (!f) { throw new UnknownEnumerationClassError({ context: { clazz } }) } if (type === 'string') { e = f!.byName[it as string] } else if (type === 'number') { e = f!.byOrdinal[it as number] } else if (it instanceof clazz) { e = it as T } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (e) { return e } throw new UnidentifiableEnumerationValueError({ message: 'unidentifiable enumeration value', context: { value: it }, }) } // eslint-disable-next-line @typescript-eslint/ban-types export function _values<T extends Enumeration<T>>(clazz: Function): T[] { const f = enums.get(clazz) if (!f) { throw new UnknownEnumerationClassError({ context: { clazz } }) } return Object.values(f.byName) } /** * Returns a convenient function to sort enumerations by their names. */ export const sortByName = <T extends Enumeration<T>>( a: Enumeration<T>, b: Enumeration<T> ) => a.name().localeCompare(b.name()) /** * Returns a convenient function to sort enumerations by their ordinals. */ export const sortByOrdinal = <T extends Enumeration<T>>( a: Enumeration<T>, b: Enumeration<T> ) => a.ordinal() - b.ordinal()