The TypeScript enum can lend powerful intellisense capabilities, but a few additional things are needed to use them in many situations. These tools help deal with situations where either flag or string enums are necessary.
Install the and module and save it to package.json.
npm install ts-enum-tools --save
Import the type of functionality needed, depending on whether an enum is a type of flag or string.
import { EnumStringsType, EnumStringsTool } from 'ts-enum-tools';
import { EnumFlagsType, EnumFlagsTool, EnumFlagsTest } from 'ts-enum-tools';
Declare a TypeScript enum beginning with unique 32 bit numbers composed of a single bit. Combination flags can then be added, using a logical OR (i.e. isSelectable | isSortable). It's also advisable to declare an inteface mirroring the enum, that better describes the boolean output provided by some of the tools.
// Declare a TypeScript enum using bit values
export enum AbFlags {
None = 0,
isSelectable = 1 << 1,
isMovable = 1 << 27,
isEditable = 1 << 28,
isSortable = 1 << 30,
isClonable = 1 << 31, // maximum!
isSelectSort = isSelectable | isSortable // example combo flag
}
// Create a map for boolean output (recommended)
export interface AbFlagsMap {
None: boolean;
isSelectable: boolean;
isMovable: boolean;
isEditable: boolean;
isSortable: boolean;
isClonable: boolean;
isSelectSort: boolean;
}
Enum flag tests can be accomplished in 3 ways. The first and most obvious method is to utilize raw test functions that have been proofed in order to reliably compare values. The enums can also be extended by generating wrapper function that produces a set of tools, and the wrapper factory can optionally set a property getter for the tools on the built-in Number primitive prototype.
The fastest methods are these simple no-frills functions. But they don't convey very much. The alternatives below are more informative in many situations where flags or strings are used.
export var EnumFlagsTest = {
has: function(val, flags) { return ((+val & +flags) === +flags); },
any: function(val, flags) { return !!(+val & +flags); },
eql: function(val, flags) { return (+val === +flags); }
}
There are 3 ways to obtain a function that wraps an enum and binds to a integer value, thereby adding some tools with additional capabilities beyond those shown above.
The most basic means of obtaining a tools function is to call the EnumFlagsType (factory method) with a basic enum type.
var abFlgFunc = EnumFlagsType<AbFlags, typeof AbFlags>(AbFlags);
Replacing the generic typeof enum shown above with an additional interface that describes the values of an enum as boolean, will enhance the tools intellisense output.
var abFlgFunc = EnumFlagsType<AbFlags, AbFlagsMap>(AbFlags);
If a property name is also provided, then it's used to assign the same tools to the Number.prototype.prop (warning: this may be illegal in some cultures).
var abFlgFunc = EnumFlagsType<AbFlags, AbFlagsMap>(AbFlags, "abFlgProp");
All of the above variations return a function that can be used to bind a value and return a set of tools which are valid for that specific value.
The tools function can be put to work on ordinary number types. These are slower than the raw methods shown first, but they are much fastest than the getters that will be shown next.
// Tools function works on enum | number types.
var abFlgEnum: AbFlags = AbFlags.isClonable | AbFlags.isSortable;
truthy(abFlgFunc(abFlgEnum).state.isClonable);
falsey(abFlgFunc(abFlgEnum).state.isMovable);
truthy(abFlgFunc(abFlgEnum).has(AbFlags.isClonable));
falsey(abFlgFunc(abFlgEnum).has(AbFlags.isMovable));
truthy(abFlgFunc(abFlgEnum).has(AbFlags.isClonable | AbFlags.isSortable));
falsey(abFlgFunc(abFlgEnum).has(AbFlags.isMovable | AbFlags.isSortable));
truthy(abFlgFunc(abFlgEnum).any(AbFlags.isMovable | AbFlags.isSortable));
truthy(abFlgFunc(abFlgEnum).any(AbFlags.isClonable));
falsey(abFlgFunc(abFlgEnum).any(AbFlags.isMovable));
truthy(abFlgFunc(abFlgEnum).eql(AbFlags.isClonable | AbFlags.isSortable));
falsey(abFlgFunc(abFlgEnum).eql(AbFlags.isClonable));
falsey(abFlgFunc(abFlgEnum).eql(AbFlags.isMovable));
truthy(abFlgFunc(abFlgEnum).toArray().length === 2);
truthy(abFlgFunc(abFlgEnum).toString().indexOf("isSortable") + 1);
For pure convenience (when the data set is not large), the getters are easier to use. The methods are only assigned to the number prototype if a property name is provided.
To access the extension through intellisense, you must add your own interface describing it. Note that the name of the property should match the string provided to the EnumType factory earlier, which in this case is "abFlgProp".
// Interface extends a Number primitive with a tools property
export interface AbNumber extends Number {
abFlgProp?: EnumFlagsTool<AbFlags, AbFlagsMap>;
}
// Tools properties are available on extended Number types (i.e. AbNumber)
var abFlgVal: AbNumber = AbFlags.isClonable | AbFlags.isSortable;
truthy(abFlgVal.abFlgProp.state.isClonable);
falsey(abFlgVal.abFlgProp.state.isMovable);
truthy(abFlgVal.abFlgProp.has(AbFlags.isClonable));
falsey(abFlgVal.abFlgProp.has(AbFlags.isMovable));
truthy(abFlgVal.abFlgProp.has(AbFlags.isClonable | AbFlags.isSortable));
falsey(abFlgVal.abFlgProp.has(AbFlags.isMovable | AbFlags.isSortable));
truthy(abFlgVal.abFlgProp.any(AbFlags.isMovable | AbFlags.isSortable));
truthy(abFlgVal.abFlgProp.any(AbFlags.isClonable));
falsey(abFlgVal.abFlgProp.any(AbFlags.isMovable));
truthy(abFlgVal.abFlgProp.eql(AbFlags.isClonable | AbFlags.isSortable));
falsey(abFlgVal.abFlgProp.eql(AbFlags.isClonable));
falsey(abFlgVal.abFlgProp.eql(AbFlags.isMovable));
truthy(abFlgVal.abFlgProp.toArray().length === 2);
truthy(abFlgVal.abFlgProp.toString().indexOf("isSortable") + 1);
For additional information on performance and comparisons, see the Test results for flags far below.
Declare a TypeScript enum using string values. Since these are not officialy supported in TypeScript yet, it is necessary to cast them as a type of 'any'. It's also advisable to declare an interface mirroring the enum, that better describes the string or boolean output provided by some of the tools.
// Declare a TypeScript enum using unique string values
export enum AbStrings {
None = <any> "none",
Select = <any> "sel",
Move = <any> "mov",
Edit = <any> "edit",
Sort = <any> "sort",
Clone = <any> "clone"
}
// Create a map for string output and comparison
export interface AbStringsMap {
None: any;
Select: any;
Move: any;
Edit: any;
Sort: any;
Clone: any;
}
Get a wrapper function that accepts a String and returns a set of tools. The same variations for the factory method that are available on number enum types also apply to string types.
var abStrFunc = EnumStringsType<AbStrings, AbStringsMap>(AbStrings, "abStrProp");
The tools function includes getters for key and val that are typed as string. This helps reduce the
need to cast the enum to a string.
assert(abStrFunc.key.Clone === "Clone");
assert(abStrFunc.val.Clone === "clone");
The tools function can be put to work on ordinary string types.
// Tools function works on enum | string types
var abStrEnum: AbStrings = AbStrings.Clone;
truthy(abStrFunc(abStrEnum).state.Clone);
falsey(abStrFunc(abStrEnum).state.Select);
truthy(abStrFunc(abStrEnum).equals(AbStrings.Clone));
falsey(abStrFunc(abStrEnum).equals(AbStrings.Move));
truthy(abStrFunc(abStrEnum).toStringKey() === "Clone");
truthy(abStrFunc(abStrEnum).toStringVal() === "clone");
As previously mentioned, the getters are easier to use, but they may be slower. The methods are only assigned to the string prototype if a property name is provided.
// Add Interface that extends a String with a tools property
export interface AbString extends String {
abStrProp?: EnumStringsTool<AbStrings, AbStringsMap>;
}
// Tools properties are then accessible on extended String types (i.e. AbString)
var abStrVal: AbString = abStrFunc.str.Clone;
truthy(abStrVal.abStrProp.state.Clone === true);
falsey(abStrVal.abStrProp.state.Move);
truthy(abStrFunc(abStrEnum).equals(AbStrings.Clone));
falsey(abStrVal.abStrProp.equals(AbStrings.Move));
truthy(abStrVal.abStrProp.toStringKey() === "Clone");
truthy(abStrVal.abStrProp.toStringVal() === "clone");
It may be beneficial to restrict the values that are recognized as keys. An optional filter function may be provided to do so. The function receives each potential key and returns T/F to indicate inclusion.
// Filter asserts that valid keys are capitalized
var abStrFiltered = EnumStringsType<AbStrings, AbStringsMap>(AbStrings, "abStrFiltered",
function(k) { return (k != k.toLowerCase()); });
// Outputs the string enum with filtered keys
// the bi-directional enum values are thereby excluded (i.e. enum[val] = key)
console.log(abStrFiltered.toArray());
[
{ key: 'None', val: 'none' },
{ key: 'Select', val: 'sel' },
{ key: 'Move', val: 'mov' },
{ key: 'Edit', val: 'edit' },
{ key: 'Sort', val: 'sort' },
{ key: 'Clone', val: 'clone' }
]
The following output give an idea of the performance of the flag tests. The tools function is considerably faster than the tools prototype methods. This should be taken into consideration when the amount of data being processed is significant.
All times are reported in milliseconds:
functions properties baseline (raw)
Nodejs 71 352 60
Chrome 82 408 76
FireFox 50 580 58
IE 1537 840 1500
Given that flags hold discreet numerical values and their importance to an application's stability, the tests warrant some rigor. The output of the tests also gives an idea how different flag combination scenarios are handled.
Browser based tests are identical and can be invoked by opening the html file in the tests folder. The page should load without the need for hosting. These tests were already ran in the latest version of major desktop browsers. Kindly report any issues found in other scenarios.
You can also include the mocha/should based tests in your own project, by simply importing them into a file within your local tests path. These statements include a module without any local references.
import 'ts-enum-tools/tests'; // TS
require('ts-enum-tools/tests'); // JS
EnumStringsType
Various logic tests
√ should clone
√ should not move
√ should not sort
√ should have property of Clone that is true
√ should have property of Move that is false
√ should ouput a string
Outlier checks
√ should maintain closure integrity when re-used
EnumFlagsType: Various tests
Rigorous logic tests
√ should handle case val(1000) > has(1000):t any(1000):t eql(1000):t state[1000]:t state[0100]:f
√ should handle case val(1100) > has(1100):t any(1100):t eql(1100):t state[1000]:t state[0100]:t
√ should handle case val(0100) > has(1000):f any(1000):f eql(1000):f state[1000]:f state[0100]:t
√ should handle case val(1100) > has(0100):t any(0100):t eql(0100):f state[1000]:t state[0100]:t
√ should handle case val(0100) > has(1100):f any(1100):t eql(1100):f state[1000]:f state[0100]:t
√ should handle case val(0000) > has(1000):f any(1000):t eql(1000):f state[1000]:f state[0100]:f
√ should handle case val(0000) > has(0000):t any(0000):f eql(0000):t state[0000]:t state[0100]:f
Miscellaneous logic tests
√ should handle invalid bit combinations in values and arguments
√ should support various flags
√ should support various properties
√ should return an array consisting of (2) flags
√ should ouput a string similar to: "isSortable | isClonable"
Outlier checks
√ should maintain closure integrity and support re-use
√ should be immutable value when using function methods
√ should not be immutable value when using prototype properties
Performance checks
√ should perform function(value) tools over (1000000) iterations (64ms)
√ should perform using value.has property over (1000000) iterations (291ms)
√ should perform using value.any property over (1000000) iterations (286ms)
√ should perform using value.state property over (1000000) iterations (312ms)
√ inline logical operation comparison baseline (5000000) iterations (64ms)
27 passing (1s)