enum-plus
Version:
A drop-in replacement for native enum. Like native enum but much better!
298 lines (285 loc) • 9.22 kB
JavaScript
import { EnumItemClass } from "./enum-item.js";
import { IS_ENUM_ITEMS, KEYS, VALUES } from "./utils.js";
/**
* Enum items array, mostly are simple wrappers for EnumCollectionClass
*
* @template T Type of the initialization data of the enum collection
*
* @class EnumItemsArray
*
* @extends {EnumItemClass<T, K, V>[]}
*
* @implements {IEnumItems<T, K, V>}
*/
export class EnumItemsArray extends Array {
__raw__;
/**
* - **EN:** A boolean value indicates that this is an enum items array.
* - **CN:** 布尔值,表示这是一个枚举项数组
*/
// Do not use readonly field here, because don't want print this field in Node.js
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
get [IS_ENUM_ITEMS]() {
return true;
}
[KEYS];
[VALUES];
labels;
named;
meta;
_runtimeError;
/**
* Instantiate an enum items array
*
* @memberof EnumItemsArray
*
* @param {T} raw Original initialization data object
* @param {EnumItemOptions<T[K], K, V, P> | undefined} options Enum item options
*/
constructor(raw, options) {
super();
// Do not use class field here, because don't want print this field in Node.js
Object.defineProperty(this, '__raw__', {
value: raw,
enumerable: false,
writable: false,
configurable: false
});
// Generate keys array
// exclude number keys with a "reverse mapping" value, it means those "reverse mapping" keys of number enums
const keys = parseKeys(raw);
const parsed = keys.map(key => parseEnumItem(raw[key], key));
this[KEYS] = keys;
Object.freeze(keys);
const items = [];
const meta = {};
this.meta = meta;
const named = {};
this.named = named;
keys.forEach((key, index) => {
const {
value,
label
} = parsed[index];
const item = new EnumItemClass(key, value, label, raw[key], options);
items.push(item);
this.push(item);
named[key] = item;
// Collect custom meta fields
const itemRaw = raw[key];
if (itemRaw && typeof itemRaw === 'object') {
Object.keys(itemRaw).forEach(k => {
const metaKey = k;
if (metaKey !== 'key' && metaKey !== 'value' && metaKey !== 'label') {
if (meta[metaKey] == null) {
meta[metaKey] = [];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const metaValue = itemRaw[metaKey];
if (metaValue != null) {
meta[metaKey].push(metaValue);
}
}
});
}
});
// Freeze meta arrays
Object.keys(meta).forEach(k => {
Object.freeze(meta[k]);
});
// Generate values array
const values = parsed.map(item => item.value);
this[VALUES] = values;
Object.freeze(values);
// Generate labels array
Object.defineProperty(this, 'labels', {
get: function () {
// Cannot save to static array because labels may be localized contents
// Should not use `items` in the closure because the getter function cannot be fully serialized
return Array.from(this).map(item => item.label);
},
enumerable: true,
configurable: false
});
this._runtimeError = undefined;
Object.defineProperty(this, '_runtimeError', {
value: function (name) {
return `The ${name} property of the enumeration is only allowed to be used to declare the ts type, and cannot be accessed at runtime! Please use the typeof operator in the ts type, for example: typeof Week.${name}`;
},
writable: false,
enumerable: false,
configurable: false
});
}
[Symbol.hasInstance](instance) {
// intentionally use == to support both number and string format value
return this.some(
// eslint-disable-next-line eqeqeq
i => instance == i.value || instance === i.key);
}
label(keyOrValue) {
// Find by value, then try key
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (this.find(i => i.value === keyOrValue) ?? this.find(i => i.key === keyOrValue))?.label;
}
key(value) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return this.find(i => i.value === value)?.key;
}
raw(keyOrValue) {
if (keyOrValue == null) {
// Return the original initialization object
return this.__raw__;
} else {
// Find by key
if (Object.keys(this.__raw__).some(k => k === keyOrValue)) {
return this.__raw__[keyOrValue];
}
// Find by value
const itemByValue = this.find(i => i.value === keyOrValue);
if (itemByValue) {
return itemByValue.raw;
}
return undefined;
}
}
has(keyOrValue) {
return this.some(i => i.value === keyOrValue || i.key === keyOrValue);
}
findBy(field, value) {
return this.find(item => {
if (field === 'key' || field === 'value') {
return item[field] === value;
} else if (field === 'label') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return item.raw?.label === value || item.label === value;
} else {
// For other fields, use the raw object to find
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return item.raw?.[field] === value;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
});
}
toList(config) {
const {
valueField = 'value',
labelField = 'label',
extra
} = config ?? {};
return Array.from(this).map(item => {
const valueFieldName = typeof valueField === 'function' ? valueField(item) : valueField;
const labelFieldName = typeof labelField === 'function' ? labelField(item) : labelField;
const extraData = extra ? extra(item) : {};
const listItem = {
[valueFieldName]: item.value,
[labelFieldName]: item.label,
...extraData
};
return listItem;
});
}
toMap(config) {
if (!config) {
return this.reduce((prev, cur) => {
prev[cur.value] = cur.label;
return prev;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{});
}
const {
keySelector = 'value',
valueSelector = 'label'
} = config;
return this.reduce((prev, cur) => {
let key;
if (typeof keySelector === 'function') {
key = keySelector(cur);
} else {
key = cur[keySelector];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let value;
if (typeof valueSelector === 'function') {
value = valueSelector(cur);
} else {
value = cur[valueSelector];
}
prev[key] = value;
return prev;
}, {});
}
/** Stub method, only for typing usages, not for runtime calling */
get valueType() {
throw new Error(this._runtimeError('valueType'));
}
/** Stub method, only for typing usages, not for runtime calling */
get keyType() {
throw new Error(this._runtimeError('keyType'));
}
/** Stub method, only for typing usages, not for runtime calling */
get rawType() {
throw new Error(this._runtimeError('rawType'));
}
}
/**
* - **EN:** Enum item collection interface, excluding members inherited from the array
* - **CN:** 枚举项集合接口,不包含从数组集成的成员
*
* @template T The type of enum initialization | 枚举初始化的类型
* @template K The type of enum keys | 枚举键的类型
* @template V The type of enum values | 枚举值的类型
*
* @interface IEnumItems
*/
// typeof IS_ENUM_ITEMS | typeof ITEMS | typeof KEYS | typeof VALUES | 'labels' | 'meta' | 'named'
/** More options for the options method */
export function parseKeys(raw) {
return Object.keys(raw).filter(k => !(/^-?\d+$/.test(k) && k === `${raw[raw[k]] ?? ''}`));
}
function parseEnumItem(init, key) {
let value;
let label;
if (init != null) {
if (typeof init === 'number' || typeof init === 'string' || typeof init === 'symbol') {
value = init;
label = key;
} else if (typeof init === 'object') {
// Initialize using object
if (Object.prototype.toString.call(init) === '[object Object]') {
if ('value' in init && Object.keys(init).some(k => k === 'value')) {
// type of {value, label}
value = init.value ?? key;
if ('label' in init && Object.keys(init).some(k => k === 'label')) {
label = init.label;
} else {
label = key;
}
} else if ('label' in init && Object.keys(init).some(k => k === 'label')) {
// typeof {label}
value = key;
label = init.label ?? key;
} else {
// {} empty object
value = key;
label = key;
}
} else {
// Probably Date, RegExp and other primitive types
value = init;
label = key;
}
} else {
throw new Error(`Invalid enum item: ${JSON.stringify(init)}`);
}
} else {
value = key;
label = key;
}
return {
value,
label
};
}
//# sourceMappingURL=enum-items.js.map