butter-enums
Version:
Typesafe specialized enums for TypeScript - Smooth like butter
177 lines (176 loc) • 6.73 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ButterKeyedEnum = ButterKeyedEnum;
const deep_freeze_es6_1 = __importDefault(require("deep-freeze-es6"));
/**
* Butter Keyed Enum
*
* Keyed enums are enums that are created with an object of objects.
* They can therefore have type safety when converting back and forth between enums and objects.
*
* E.g.
*
* ```typescript
* const colorsEnum = ButterKeyedEnum({
* green: {
* emoji: '🟩',
* hex: '#00FF00',
* },
* })
*
* colorsEnum.enum.green.emoji // '🟩'
* colorsEnum.enum.green.key // 'green'
* colorsEnum.get('green') // { emoji: '🟩', hex: '#00FF00', key: 'green' }
* colorsEnum.keys // ['green']
* ```
*
* @template KeyName - The name of the property to hoist the key into. Defaults to `"key"`.
* @template T - The original enum-like object whose keys should not conflict with `keyName` in inner objects.
* @template TTuple - The tuple of values derived from `T` that will represent the enum values.
* @template TTuple - The final result tuple validated against the keys of `T`.
*
* @param {T} enumObject - The original object representing the enum-like mapping.
* @param {Object} options - Configuration options.
* @param {KeyName} [options.keyName="key"] - The name of the key to inject into each value.
* @param {(enumObject: Readonly<HoistKeyToInner<T, KeyName>>) => TTuple} options.tupleFactory - A factory function
* that takes the modified enum object with keys hoisted and returns a tuple. It must include all keys from `enumObject`.
*
* This is required because typescript cannot convert from a union to a tuple with
* * Guaranteed order
* * Better performance than O(n^2)
* See https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type
*
* So, we have to provide a tuple factory. It constrains the tuple to make sure you're not missing any values.
* Making our typescript compiler happy.
*
* @returns {void} This function does not return anything directly, but can be used to enforce compile-time constraints
* and build strongly typed enums using TypeScript's type system.
*
* @throws {TypeError} If any object in `enumObject` already contains the `keyName` property, it will result in a type error.
* @throws {Error} If the `tupleFactory` does not return a tuple that includes all keys, a compile-time type error will occur.
*/
function ButterKeyedEnum(enumObject, options) {
const $enum = (0, deep_freeze_es6_1.default)(Object.fromEntries(Object.entries(enumObject).map(([key, value]) => [
key,
{ ...value, [options?.keyName ?? "key"]: key },
])));
const $tuple = options?.tupleFactory
? (0, deep_freeze_es6_1.default)(options.tupleFactory($enum))
: [];
function getMany(keys) {
return keys.map((key) => $enum[key]);
}
return {
/// TUPLE DEPENDENT
///
/**
* An ordered array of enum values as specified by the tupleFactory function
*
* @type {TTuple} The tuple of enum values in the order defined by tupleFactory
*/
get tuple() {
if ($tuple.length === 0) {
return {
__error__: "Provide tupleFactory if you want a tuple, and ensure it's not empty",
value: undefined,
};
}
return $tuple;
},
/**
* An ordered array of keys as specified by the tupleFactory function
*
* @type {TTuple extends [] ? ButterEnumsErrorMessage<"Provide tupleFactory if you want ordered keys, and ensure it's not empty"> : { [TIndex in keyof TTuple]: TTuple[TIndex][KeyName]; }} The ordered keys of the enum
*/
get orderedKeys() {
if ($tuple.length === 0) {
return {
__error__: "Provide tupleFactory if you want ordered keys, and ensure it's not empty",
value: undefined,
};
}
return $tuple.map((value) => value[options?.keyName ?? "key"]);
},
/**
* Maps a property of the tuple to an array of values
*
* @example
* ```typescript
* const colorsEnum = ButterKeyedEnum({
* green: {
* emoji: '🟩',
* hex: '#00FF00',
* },
* red: {
* emoji: '🟥',
* hex: '#FF0000',
* },
* }, {
* tupleFactory: (enumObject) => [
* enumObject.green,
* enumObject.red,
* ]
* })
*
* colorsEnum.getTupleValuesByProperty('emoji') // ['🟩', '🟥']
* ```
*
* @param property The property to map
* @returns An array of values from the tuple
*/
getTupleValuesByProperty(property) {
if ($tuple.length === 0) {
return {
__error__: "Provide tupleFactory if you want a tuple, and ensure it's not empty",
value: undefined,
};
}
return $tuple.map((value) => value[property]);
},
/// NON-TUPLE DEPENDENT
/**
* The enum object
*
* @type {Readonly<TEnum>} The enum object with keys hoisted into each value
*/
enum: $enum,
/**
* Gets a value by key
*
* @param key The key to retrieve the value for
* @returns {TEnum[keyof TEnum] | undefined} The value for the given key or undefined if the key doesn't exist
*/
get(key) {
return $enum[key];
},
getMany,
/**
* All keys in the enum
*
* @returns {(keyof TEnum)[]} All keys in the enum
*/
get keys() {
return Object.keys($enum);
},
/**
* All values in the enum
*
* @returns {TEnum[keyof TEnum][]} All values in the enum
*/
get values() {
return Object.values($enum);
},
/**
* Finds a value by predicate
*
* @param predicate A function that tests each value for a condition
* @returns {TEnum[keyof TEnum] | undefined} The first value that matches the predicate or undefined if no match is found
*/
find(predicate) {
return Object.values($enum).find((value, key) => predicate(value, key, $enum));
},
};
}