Skip to content

Commit caf5ddc

Browse files
committed
feat: refactor enum detecting to use ENUM_ITEM and ENUM_COLLECTION symbols
1 parent 5e7725e commit caf5ddc

File tree

13 files changed

+129
-48
lines changed

13 files changed

+129
-48
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
# enum-plus Changelog
44

5+
## 2.2.12
6+
7+
2025-5-20
8+
9+
### Features
10+
11+
- ✨ Exports three new symbols `ENUM_ITEM``ENUM_ITEMS` and `ENUM_COLLECTION`.
12+
- ✨ Add `[ENUM_COLLECTION] = true` to the `Enum` class, which is used to indicates that this is as an enum collection.
13+
- ✨ Add `[ENUM_ITEM] = true` to the `EnumItem` class, which is used to indicates that this is as an enum item.
14+
- ✨ Add `[ENUM_ITEMS] = true` to the `Enum.items`, which is used to indicates that this is as an enum items array.
15+
16+
### Breaking Changes
17+
18+
- 💣 Remove `[Symbol.toStringTag] = "EnumItem"` from `EnumItem` class. The value of `Object.prototype.toString.call(enumItem)` is changed from `[object EnumItem]` to `[object Object]`. If you are relying on this, please use `enumItem[ENUM_ITEM] === true` instead.
19+
- 💣 Remove `[Symbol.toStringTag] = "EnumCollection"` from `Enum` class. The value of `Object.prototype.toString.call(enum)` is changed from `[object EnumCollection]` to `[object Array]`. If you are relying on this, please use `enum[ENUM_COLLECTION] === true` instead.
20+
521
## 2.2.11
622

723
2025-5-15

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "enum-plus",
3-
"version": "2.2.11",
3+
"version": "2.2.12",
44
"description": "A drop-in replacement for native enum. Like native enum but much better!",
55
"keywords": [
66
"enum",

src/enum-collection.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EnumItemClass } from './enum-item';
2-
import { EnumValuesArray } from './enum-values';
2+
import { EnumItemsArray } from './enum-values';
33
import type {
44
BooleanFirstOptionConfig,
55
ColumnFilterItem,
@@ -10,14 +10,14 @@ import type {
1010
EnumKey,
1111
EnumValue,
1212
FindEnumKeyByValue,
13-
IEnumValues,
13+
IEnumItems,
1414
MenuItemOption,
1515
ObjectFirstOptionConfig,
1616
StandardEnumItemInit,
1717
ToSelectConfig,
1818
ValueTypeFromSingleInit,
1919
} from './types';
20-
import { ITEMS, KEYS, VALUES } from './utils';
20+
import { ENUM_COLLECTION, ITEMS, KEYS, VALUES } from './utils';
2121

2222
/**
2323
* **EN:** Enum collection extension base class, used to extend the Enums
@@ -42,10 +42,16 @@ export class EnumCollectionClass<
4242
V extends EnumValue = ValueTypeFromSingleInit<T[K], K>,
4343
>
4444
extends EnumExtensionClass<T, K, V>
45-
implements IEnumValues<T, K, V>
45+
implements IEnumItems<T, K, V>
4646
{
47-
readonly items!: EnumValuesArray<T, K, V>;
47+
readonly items!: EnumItemsArray<T, K, V>;
4848
readonly keys!: K[];
49+
/**
50+
* **EN:** A boolean value indicates that this is an enum collection instance.
51+
*
52+
* **CN:** 布尔值,表示这是一个枚举集合实例
53+
*/
54+
readonly [ENUM_COLLECTION] = true;
4955

5056
constructor(init: T = {} as T, options?: EnumItemOptions) {
5157
super();
@@ -64,7 +70,7 @@ export class EnumCollectionClass<
6470
this[Object.keys(init).some((k) => k === 'keys') ? KEYS : 'keys'] = keys;
6571

6672
// Build enum item data
67-
const items = new EnumValuesArray<T, K, V>(
73+
const items = new EnumItemsArray<T, K, V>(
6874
init,
6975
options,
7076
...keys.map((key, index) => {
@@ -77,9 +83,6 @@ export class EnumCollectionClass<
7783
// @ts-expect-error: because use VALUES to avoid naming conflicts in case of 'values' field name is taken
7884
this[Object.keys(init).some((k) => k === 'values') ? VALUES : 'values'] = items;
7985

80-
// Override some system methods
81-
// @ts-expect-error: because override Object.toString method for better type display
82-
this[Symbol.toStringTag] = 'EnumCollection';
8386
// Override the `instanceof` operator rule
8487
// @ts-expect-error: because override the instanceof operator
8588
this[Symbol.hasInstance] = (instance: unknown): boolean => {

src/enum-item.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Enum } from './enum';
22
import type { EnumItemInit, EnumItemOptions, EnumKey, EnumValue, ValueTypeFromSingleInit } from './types';
3+
import { ENUM_ITEM } from './utils';
34

45
/**
56
* Enum item class
@@ -25,6 +26,12 @@ export class EnumItemClass<
2526

2627
/** Original initialization object */
2728
readonly raw: T;
29+
/**
30+
* **EN:** A boolean value indicates that this is an enum item instance.
31+
*
32+
* **CN:** 布尔值,表示这是一个枚举项实例
33+
*/
34+
[ENUM_ITEM] = true;
2835

2936
#localize: NonNullable<EnumItemOptions['localize']>;
3037
#localizedProxy = new Proxy(this, {
@@ -96,9 +103,6 @@ export class EnumItemClass<
96103
return content;
97104
};
98105

99-
// Override some system methods
100-
// @ts-expect-error: because override Object.toString method to display type more friendly
101-
this[Symbol.toStringTag] = 'EnumItem';
102106
// @ts-expect-error: because override Object.toPrimitive method to return enum value
103107
this[Symbol.toPrimitive] = (hint: 'number' | 'string' | 'default'): V | string => {
104108
if (hint === 'number') {

src/enum-values.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import type {
1010
EnumKey,
1111
EnumValue,
1212
FindEnumKeyByValue,
13-
IEnumValues,
13+
IEnumItems,
1414
MenuItemOption,
1515
ObjectFirstOptionConfig,
1616
ToSelectConfig,
1717
ValueMap,
1818
ValueTypeFromSingleInit,
1919
} from './types';
20+
import { ENUM_ITEMS } from './utils';
2021

2122
/**
2223
* Enum items array, mostly are simple wrappers for EnumCollectionClass
@@ -31,13 +32,13 @@ import type {
3132
*
3233
* @implements {IEnumValues<T, K, V>}
3334
*/
34-
export class EnumValuesArray<
35+
export class EnumItemsArray<
3536
T extends EnumInit<K, V>,
3637
K extends EnumKey<T> = EnumKey<T>,
3738
V extends EnumValue = ValueTypeFromSingleInit<T[K], K>,
3839
>
3940
extends Array<EnumItemClass<T[K], K, V>>
40-
implements IEnumValues<T, K, V>
41+
implements IEnumItems<T, K, V>
4142
{
4243
#raw: T;
4344
#localize: NonNullable<EnumItemOptions['localize']>;
@@ -62,6 +63,12 @@ export class EnumValuesArray<
6263
return content;
6364
};
6465
}
66+
/**
67+
* **EN:** A boolean value indicates that this is an enum items array.
68+
*
69+
* **CN:** 布尔值,表示这是一个枚举项数组
70+
*/
71+
[ENUM_ITEMS] = true;
6572

6673
label(keyOrValue?: string | number): string | undefined {
6774
// First find by value, then find by key
@@ -199,3 +206,10 @@ export class EnumValuesArray<
199206
);
200207
}
201208
}
209+
210+
/** @deprecated Use `EnumItemsArray` instead */
211+
export class EnumValuesArray<
212+
T extends EnumInit<K, V>,
213+
K extends EnumKey<T> = EnumKey<T>,
214+
V extends EnumValue = ValueTypeFromSingleInit<T[K], K>,
215+
> extends EnumItemsArray<T, K, V> {}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type {
1616
} from './types';
1717
export type { EnumItemClass } from './enum-item';
1818

19-
export { KEYS, ITEMS, VALUES } from './utils';
19+
export { KEYS, ITEMS, VALUES, ENUM_ITEM, ENUM_ITEMS, ENUM_COLLECTION } from './utils';
2020
export { defaultLocalize };
2121
export { Enum } from './enum';
2222

src/types.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ export type IEnum<
6363
T extends EnumInit<K, V>,
6464
K extends EnumKey<T> = EnumKey<T>,
6565
V extends EnumValue = ValueTypeFromSingleInit<T[K], K>,
66-
> = IEnumValues<T, K, V> & {
66+
> = IEnumItems<T, K, V> & {
6767
// 初始化对象里的枚举字段
6868
[key in K]: ValueTypeFromSingleInit<T[key], key, T[K] extends number | undefined ? number : key>;
6969
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7070
} & (T extends { items: any }
7171
? {
7272
// 防止values名称冲突
73-
[ITEMS]: EnumItemClass<T[K], K, V>[] & IEnumValues<T, K, V>;
73+
[ITEMS]: EnumItemClass<T[K], K, V>[] & IEnumItems<T, K, V>;
7474
}
7575
: {
7676
/**
@@ -84,17 +84,17 @@ export type IEnum<
8484
*
8585
* 仅支持 ReadonlyArray<T> 中的只读方法,不支持push、pop等任何修改的方法
8686
*/
87-
items: EnumItemClass<T[K], K, V>[] & IEnumValues<T, K, V>;
87+
items: EnumItemClass<T[K], K, V>[] & IEnumItems<T, K, V>;
8888
}) &
8989
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9090
(T extends { values: any }
9191
? {
9292
// 防止values名称冲突
93-
[VALUES]: EnumItemClass<T[K], K, V>[] & IEnumValues<T, K, V>;
93+
[VALUES]: EnumItemClass<T[K], K, V>[] & IEnumItems<T, K, V>;
9494
}
9595
: {
9696
/** @deprecated Use `items` instead */
97-
values: EnumItemClass<T[K], K, V>[] & IEnumValues<T, K, V>;
97+
values: EnumItemClass<T[K], K, V>[] & IEnumItems<T, K, V>;
9898
}) &
9999
// eslint-disable-next-line @typescript-eslint/no-explicit-any
100100
(T extends { keys: any }
@@ -123,11 +123,9 @@ export type IEnum<
123123
*
124124
* @template T Enum collection initialization data type | 枚举集合初始化数据的类型
125125
*
126-
* @export
127-
*
128-
* @interface IEnumValues
126+
* @interface IEnumItems
129127
*/
130-
export interface IEnumValues<
128+
export interface IEnumItems<
131129
T extends EnumInit<K, V>,
132130
K extends EnumKey<T> = EnumKey<T>,
133131
V extends EnumValue = ValueTypeFromSingleInit<T[K], K>,
@@ -361,6 +359,13 @@ export interface IEnumValues<
361359
rawType: T[K];
362360
}
363361

362+
/** @deprecated use `IEnumItems` instead */
363+
export type IEnumValues<
364+
T extends EnumInit<K, V>,
365+
K extends EnumKey<T> = EnumKey<T>,
366+
V extends EnumValue = ValueTypeFromSingleInit<T[K], K>,
367+
> = IEnumItems<T, K, V>;
368+
364369
// eslint-disable-next-line @typescript-eslint/ban-types
365370
export type EnumItemLabel = EnumLocaleExtends['LocaleKeys'] | (string & {});
366371

src/utils.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,47 @@ import type { EnumItemOptions } from './types';
66
*
77
* **CN:** 枚举`values`集合的别名。如果枚举中包含了`values`的同名字段,可以通过此Symbol作为字段名来访问
88
*/
9-
export const VALUES = Symbol('[values]');
9+
export const VALUES = Symbol.for('[values]');
1010

1111
/**
1212
* **EN:** Alias of `items`. If the enum contains a field with the same name as `items`, you can
1313
* access it by this Symbol as the field name
1414
*
1515
* **CN:** 枚举`items`集合的别名。如果枚举中包含了`items`的同名字段,可以通过此Symbol作为字段名来访问
1616
*/
17-
export const ITEMS = Symbol('[items]');
17+
export const ITEMS = Symbol.for('[items]');
1818

1919
/**
2020
* **EN:** Alias of `keys`. If the enum contains a field with the same name as `keys`, you can
2121
* access it by this Symbol as the field name
2222
*
2323
* **CN:** 枚举keys集合的别名。如果枚举中包含了`keys`的同名字段,可以通过此Symbol作为字段名来访问
2424
*/
25-
export const KEYS = Symbol('[keys]');
25+
export const KEYS = Symbol.for('[keys]');
26+
27+
/**
28+
* **EN:** Check whether the object is an instance of `EnumItem`. If the object has this field and
29+
* its value is `true`, it is an `EnumItem` instance.
30+
*
31+
* **CN:** 判断对象是否 `EnumItem` 实例,如果对象存在此字段且值为`true`,则是 `EnumItem` 实例
32+
*/
33+
export const ENUM_ITEM = Symbol.for('[EnumItem]');
34+
35+
/**
36+
* **EN:** Check whether the object is an instance of `EnumCollection`. If the object has this field
37+
* and its value is `true`, it is an `EnumCollection` instance.
38+
*
39+
* **CN:** 判断对象是否 `EnumCollection` 实例,如果对象存在此字段且值为`true`,则是 `EnumCollection` 实例
40+
*/
41+
export const ENUM_COLLECTION = Symbol.for('[EnumCollection]');
42+
43+
/**
44+
* **EN:** Check whether the object is an instance of `EnumItems`. If the object has this field and
45+
* its value is `true`, it is an `EnumItems` instance.
46+
*
47+
* **CN:** 判断对象是否 `EnumItems` 实例,如果对象存在此字段且值为`true`,则是 `EnumItems` 实例
48+
*/
49+
export const ENUM_ITEMS = Symbol.for('[EnumItems]');
2650

2751
/**
2852
* **EN:** Default global localization function, only used to resolve built-in resources into

test/enum-collection.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Enum, ITEMS, KEYS, VALUES } from '@enum-plus';
1+
import { Enum, ENUM_COLLECTION, ITEMS, KEYS, VALUES } from '@enum-plus';
22
import { locales, sillyLocalize, StandardWeekConfig } from './data/week-config';
33
import { getStandardWeekData } from './data/week-data';
44
import { addEnumValuesTestSuite } from './enum-values.test';
@@ -66,7 +66,7 @@ describe('the EnumCollectionClass api', () => {
6666
)
6767
);
6868
expect(strangeEnum.keys).toBe(101);
69-
expect(strangeEnum.items.map((i) => i.key)).toEqual(Object.keys(strangeEnumConfig));
69+
expect(Array.from(strangeEnum.items).map((i) => i.key)).toEqual(Object.keys(strangeEnumConfig));
7070

7171
const strangerEnumConfig = {
7272
...strangeEnumConfig,
@@ -88,10 +88,12 @@ describe('the EnumCollectionClass api', () => {
8888
expect(toPlainEnums(strangerEnum[VALUES])).toEqual(standardEnumItems);
8989
});
9090

91-
test('[toString] should return a friendly name', () => {
91+
test('should have [ENUM_COLLECTION] property to indicate that this is an enum collection', () => {
9292
const week = Enum(StandardWeekConfig);
93-
expect(week.toString()).toBe('[object EnumCollection]');
94-
expect(Object.prototype.toString.call(week)).toBe('[object EnumCollection]');
93+
// @ts-expect-error: because ENUM_COLLECTION is hidden by the interface, but it actually exists
94+
expect(week[ENUM_COLLECTION]).toBe(true);
95+
// @ts-expect-error: because ENUM_COLLECTION equals Symbol.for('[EnumCollection]')
96+
expect(week[Symbol.for('[EnumCollection]')]).toBe(true);
9597
});
9698

9799
test('enum value/key/label should be success by the [instanceof] operator', () => {

test/enum-item.test.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Enum } from '@enum-plus';
1+
import { Enum, ENUM_ITEM } from '@enum-plus';
22
import { localeEN, StandardWeekConfig } from './data/week-config';
33

44
describe('the EnumItemClass api', () => {
@@ -29,11 +29,16 @@ describe('the EnumItemClass api', () => {
2929
expect(week.values[6].toLocaleString()).toBe(localeEN.Saturday);
3030
});
3131

32-
test('[toStringTag] should return a friendly name', () => {
32+
test('should have [ENUM_ITEM] property to indicate that this is an enum item', () => {
3333
const week = Enum(StandardWeekConfig);
34-
week.values.forEach((item) => {
35-
expect(Object.prototype.toString.call(item)).toBe('[object EnumItem]');
36-
});
34+
const firstItem = week.items[0];
35+
const lastItem = week.items[week.items.length - 1];
36+
expect(firstItem[ENUM_ITEM]).toBe(true);
37+
// @ts-expect-error: because ENUM_ITEM equals Symbol.for('[EnumItem]')
38+
expect(firstItem[Symbol.for('[EnumItem]')]).toBe(true);
39+
expect(lastItem[ENUM_ITEM]).toBe(true);
40+
// @ts-expect-error: because ENUM_ITEM equals Symbol.for('[EnumItem]')
41+
expect(lastItem[Symbol.for('[EnumItem]')]).toBe(true);
3742
});
3843

3944
test('[valueOf] should return the enum value', () => {

0 commit comments

Comments
 (0)