Skip to content

Commit f6345b7

Browse files
committed
feat: add enums.findBy method to find enum items by built-in fields and custom meta fields
1 parent 85957a8 commit f6345b7

File tree

11 files changed

+174
-16
lines changed

11 files changed

+174
-16
lines changed

enum-plus-v3.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# enum-plus v3.0 - A Major Update
22

3+
### ■■■■■■■■■■■■■■■■■■□□ (90%)
4+
35
## Features
46

57
### Codebase
@@ -8,14 +10,18 @@
810
- [x] Remove deprecated `enum.menus`
911
- [x] Remove deprecated `enum.filters`
1012
- [x] Remove deprecated `enum.valuesEnum`
13+
- [x] The following symbols have been renamed to better reflect their purpose:
14+
- `ENUM_COLLECTION` is now `IS_ENUM`
15+
- `ENUM_ITEM` is now `IS_ENUM_ITEM`
16+
- `ENUM_ITEMS` is now `IS_ENUM_ITEMS`
1117
- [x] Change the behavior of `enum.values`, now it returns an array of the member raw values. Use `enum.items` for the old behavior.
1218
- [x] Add `enum.labels` property, which returns an readonly array of the member labels.
13-
- [ ] Add `enum.find` method, which allows searching for enum items by specific field, including _meta_ fields (i.e. custom fields).
19+
- [x] Add `enum.findBy` method, which allows searching for enum items by built-in fields, and the custom _meta_ fields.
1420
- [ ] Add `enum.meta` object to aggregate all custom fields defined in the enum. The keys are the field names, and values are the raw values of each field. It's a good way of accessing custom fields without iterating through the enum items.
1521
- [x] Add `enum.toList`, method which is an alternative of `toSelect``toMenu``toFilter`. The latter methods are moving out of the core library and will be available as plugins.
1622
- [ ] Add `enum.toMap` as an alternative of `enum.toValueMap`.
1723
- [x] Add `Enum.isEnum` method to check if an object is an instance of `Enum`.
18-
- [ ] Add type assertion for `instanceof` check of EnumCollection.
24+
- [x] Add type assertion for `instanceof` check of EnumCollection.
1925
- [x] Simplify the enum initialization that no longer requires `as const` assertion. _by @otomad_
2026
- [x] Release the `UMD` module format of `enum-plus` in `umd` folder.
2127
- [x] Reuse one copy of testing code for both `Jest` and `e2e` testing.

src/enum-collection.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ export class EnumCollectionClass<
5151
* - **EN:** A boolean value indicates that this is an enum collection instance.
5252
* - **CN:** 布尔值,表示这是一个枚举集合实例
5353
*/
54-
readonly [IS_ENUM] = true;
54+
// Do not use readonly field here, because don't want print this field in Node.js
55+
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
56+
get [IS_ENUM](): true {
57+
return true;
58+
}
5559
[Symbol.hasInstance]<T>(instance: T): instance is Extract<T, K | V> {
5660
return instance instanceof this.items;
5761
}
@@ -151,6 +155,11 @@ export class EnumCollectionClass<
151155
return this.items.has(keyOrValue);
152156
}
153157

158+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
159+
findBy(...rest: Parameters<EnumItemsArray<T, K, V>['findBy']>): any {
160+
return this.items.findBy(...rest);
161+
}
162+
154163
toList(): ListItem<V, 'value', 'label'>[];
155164
toList<
156165
FOV extends string | ((item: EnumItemClass<T[K], K, V>) => string),

src/enum-item.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class EnumItemClass<
4444
* - **EN:** A boolean value indicates that this is an enum item instance.
4545
* - **CN:** 布尔值,表示这是一个枚举项实例
4646
*/
47-
readonly [IS_ENUM_ITEM] = true;
47+
private readonly [IS_ENUM_ITEM] = true;
4848
/**
4949
* Auto convert to a correct primitive type. This method is called when the object is used in a
5050
* context that requires a primitive value.
@@ -55,7 +55,8 @@ export class EnumItemClass<
5555
*
5656
* @returns V | string
5757
*/
58-
[Symbol.toPrimitive](this: EnumItemClass<T, K, V>, hint: 'number' | 'string' | 'default'): V | string {
58+
// @ts-expect-error: because don't want show `Symbol` in vscode's intellisense, it should work in background
59+
private [Symbol.toPrimitive](this: EnumItemClass<T, K, V>, hint: 'number' | 'string' | 'default'): V | string {
5960
if (hint === 'number') {
6061
// for cases like Number(value) or +value
6162
return this.valueOf();

src/enum-items.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import type {
55
EnumKey,
66
EnumValue,
77
FindEnumKeyByValue,
8+
FindKeyByMeta,
89
FindLabelByValue,
10+
FindValueByKey,
11+
FindValueByMeta,
912
ListItem,
1013
MenuItemOption,
1114
PrimitiveOf,
@@ -26,7 +29,7 @@ import { IS_ENUM_ITEMS } from './utils';
2629
* @implements {IEnumValues<T, K, V>}
2730
*/
2831
export class EnumItemsArray<
29-
T extends EnumInit<K, V>,
32+
const T extends EnumInit<K, V>,
3033
K extends EnumKey<T> = EnumKey<T>,
3134
V extends EnumValue = ValueTypeFromSingleInit<T[K], K>,
3235
>
@@ -38,7 +41,11 @@ export class EnumItemsArray<
3841
* - **EN:** A boolean value indicates that this is an enum items array.
3942
* - **CN:** 布尔值,表示这是一个枚举项数组
4043
*/
41-
readonly [IS_ENUM_ITEMS] = true;
44+
// Do not use readonly field here, because don't want print this field in Node.js
45+
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
46+
get [IS_ENUM_ITEMS](): true {
47+
return true;
48+
}
4249

4350
/**
4451
* Instantiate an enum items array
@@ -127,6 +134,41 @@ export class EnumItemsArray<
127134
return this.some((i) => i.value === keyOrValue || i.key === keyOrValue);
128135
}
129136

137+
findBy<FK extends 'key' | 'value' | 'label' | Exclude<keyof T[keyof T], 'key' | 'value' | 'label'>, FV>(
138+
field: FK,
139+
value: FV
140+
): FK extends 'key'
141+
? FV extends K
142+
? // @ts-expect-error: because the type infer is not clever enough, FV here should be one of K
143+
EnumItemClass<T[FV], FV, FindValueByKey<T, FV>>
144+
: EnumItemClass<T[K], K, V> | undefined
145+
: FK extends 'value'
146+
? FV extends V
147+
? // @ts-expect-error: because the type infer is not clever enough, FV here should be one of V
148+
EnumItemClass<T[FindEnumKeyByValue<T, FV>], FindEnumKeyByValue<T, FV>, FV>
149+
: EnumItemClass<T[K], K, V> | undefined
150+
: FK extends 'label'
151+
? EnumItemClass<T[K], K, V> | undefined
152+
: // @ts-expect-error: because the type infer is not clever enough, FK here should be one of keyof Raw
153+
FV extends T[keyof T][FK]
154+
? // @ts-expect-error: because the type infer is not clever enough, FV here should be one of T[keyof T][FK]
155+
EnumItemClass<T[FindKeyByMeta<T, FK, FV>], FindKeyByMeta<T, FK, FV>, FindValueByMeta<T, FK, FV>>
156+
: EnumItemClass<T[K], K, V> | undefined {
157+
return this.find((item) => {
158+
if (field === 'key' || field === 'value') {
159+
return item[field as keyof typeof item] === value;
160+
} else if (field === 'label') {
161+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
162+
return (item.raw as any)?.label === value || item.label === value;
163+
} else {
164+
// For other fields, use the raw object to find
165+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
166+
return (item.raw as any)?.[field] === value;
167+
}
168+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
169+
}) as any;
170+
}
171+
130172
toList(): ListItem<V, 'value', 'label'>[];
131173
toList<
132174
FOV extends string | ((item: EnumItemClass<T[K], K, V>) => string),
@@ -341,6 +383,37 @@ export interface IEnumItems<
341383
*/
342384
has(keyOrValue?: string | V): boolean;
343385

386+
/**
387+
* **EN:** Find an enumeration item by key or value, or by custom meta fields
388+
*
389+
* **CN:** 通过key或value查找枚举项,或通过自定义元字段查找
390+
*
391+
* @param field The field to search by | 要查找的字段
392+
* @param value The value to search | 要查找的值
393+
*
394+
* @returns The found enumeration item or undefined if not found | 找到的枚举项,如果未找到则返回 undefined
395+
*/
396+
findBy<FK extends 'key' | 'value' | 'label' | Exclude<keyof T[keyof T], 'key' | 'value' | 'label'>, FV>(
397+
field: FK,
398+
value: FV
399+
): FK extends 'key'
400+
? FV extends K
401+
? // @ts-expect-error: because the type infer is not clever enough, FV here should be one of K
402+
EnumItemClass<T[FV], FV, FindValueByKey<T, FV>>
403+
: EnumItemClass<T[K], K, V> | undefined
404+
: FK extends 'value'
405+
? FV extends V
406+
? // @ts-expect-error: because the type infer is not clever enough, FV here should be one of V
407+
EnumItemClass<T[FindEnumKeyByValue<T, FV>], FindEnumKeyByValue<T, FV>, FV>
408+
: EnumItemClass<T[K], K, V> | undefined
409+
: FK extends 'label'
410+
? EnumItemClass<T[K], K, V> | undefined
411+
: // @ts-expect-error: because the type infer is not clever enough, FK here should be one of keyof Raw
412+
FV extends T[keyof T][FK]
413+
? // @ts-expect-error: because the type infer is not clever enough, FV here should be one of T[keyof T][FK]
414+
EnumItemClass<T[FindKeyByMeta<T, FK, FV>], FindKeyByMeta<T, FK, FV>, FindValueByMeta<T, FK, FV>>
415+
: EnumItemClass<T[K], K, V> | undefined;
416+
344417
/**
345418
* - **EN:** Generate an object array containing all enumeration items
346419
* - **CN:** 生成一个对象数组,包含所有的枚举项

src/enum.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ export type IEnum<
201201
* - **EN:** A boolean value indicates that this is an Enum.
202202
* - **CN:** 布尔值,表示这是一个枚举类
203203
*/
204-
[IS_ENUM]: true;
204+
// this flag exists but is removed from interface, as it's replaced with isEnum method
205+
// [IS_ENUM]: true;
205206
} & {
206207
// Add enum item values, just like native enums
207208
[key in K]: ValueTypeFromSingleInit<T[key], key, T[K] extends number | undefined ? number : key>;

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export type {
88
MenuItemOption,
99
ColumnFilterItem,
1010
FindEnumKeyByValue,
11+
FindValueByKey,
12+
FindLabelByValue,
1113
ArrayToMap,
1214
} from './types';
1315
export type { LocalizeInterface } from './localize-interface';

src/types.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,35 @@ export type FindLabelByValue<T, V extends EnumValue, RAW = T[FindEnumKeyByValue<
174174
? string
175175
: FindEnumKeyByValue<T, V>;
176176

177+
/**
178+
* - **EN:** Find the value of the enumeration item by key
179+
* - **CN:** 通过key查找枚举项的值
180+
*
181+
* @template T Enum collection initialization data type | 枚举集合初始化数据的类型
182+
* @template K Enum key type | 枚举key的类型
183+
*/
184+
export type FindValueByKey<T, K extends EnumKey<T>> = T[K] extends EnumValue
185+
? T[K]
186+
: T[K] extends StandardEnumItemInit<infer V>
187+
? V
188+
: T[K] extends ValueOnlyEnumItemInit<infer V>
189+
? V
190+
: T[K] extends LabelOnlyEnumItemInit
191+
? K
192+
: T[K] extends CompactEnumItemInit
193+
? K
194+
: T[K] extends undefined
195+
? K
196+
: never;
197+
198+
export type FindKeyByMeta<T, MK extends keyof T[keyof T], MV> = {
199+
[K in keyof T]: T[K] extends Record<MK, MV> ? K : never;
200+
}[keyof T];
201+
202+
export type FindValueByMeta<T, MK extends keyof T[keyof T], MV> = {
203+
[K in keyof T]: T[K] extends Record<MK, MV> ? T[K][MK] : never;
204+
}[keyof T];
205+
177206
export type PrimitiveOf<T> = T extends string
178207
? string
179208
: T extends number

test/test-suites/enum-collection.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ const testEnumCollection = (engine: TestEngineBase) => {
6363
engine.expect(week.label(1)).toBe(week.items.label(1));
6464
engine.expect(week.key(1)).toBe(week.items.key(1));
6565
engine.expect(week.has(1)).toBe(week.items.has(1));
66+
engine.expect(week.findBy('key', 'Monday')).toBe(week.items.findBy('key', 'Monday'));
67+
engine.expect(week.findBy('value', 1)).toBe(week.items.findBy('value', 1));
68+
engine.expect(week.findBy('label', 'weekday.monday')).toBe(week.items.findBy('label', 'weekday.monday'));
69+
engine.expect(week.findBy('status', 'success')).toBe(week.items.findBy('status', 'success'));
6670
engine.expect(week.raw()).toBe(week.items.raw());
6771
engine.expect(week.toList()).toEqual(week.items.toList());
6872
engine.expect(week.toMenu()).toEqual(week.items.toMenu());
@@ -166,7 +170,7 @@ const testEnumCollection = (engine: TestEngineBase) => {
166170
({ week, IS_ENUM }) => {
167171
// @ts-expect-error: because IS_ENUM is hidden by the interface, but it actually exists
168172
engine.expect(week[IS_ENUM]).toBe(true);
169-
engine.expect(week[IS_ENUM_IN_NODE]).toBe(true);
173+
engine.expect(IS_ENUM_IN_NODE in week && week[IS_ENUM_IN_NODE]).toBe(true);
170174
// @ts-expect-error: because IS_ENUM and Symbol.for('[IsEnum]') are equal
171175
engine.expect(week[Symbol.for('[IsEnum]')]).toBe(true);
172176
}

test/test-suites/enum-items.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,38 @@ export function addEnumValuesTestSuite(engine: TestEngineBase) {
5454
}
5555
);
5656

57+
engine.test(
58+
'enums.find should be able to find enum item by key, value or custom meta fields',
59+
({ EnumPlus: { Enum }, WeekConfig: { StandardWeekConfig, WeekCompactConfig } }) => {
60+
const weekEnum = Enum(StandardWeekConfig);
61+
const compactWeekEnum = Enum(WeekCompactConfig);
62+
return { weekEnum, compactWeekEnum };
63+
},
64+
({ weekEnum, compactWeekEnum }) => {
65+
engine.expect(weekEnum.findBy('key', 'Monday')).toBe(weekEnum.items[1]);
66+
engine.expect(weekEnum.findBy('key', 'Saturday')).toBe(weekEnum.items[6]);
67+
engine.expect(weekEnum.findBy('key', 'Invalid Key')).toBe(undefined);
68+
engine.expect(weekEnum.findBy('value', 1)).toBe(weekEnum.items[1]);
69+
engine.expect(weekEnum.findBy('value', 6)).toBe(weekEnum.items[6]);
70+
engine.expect(weekEnum.findBy('value', 99)).toBe(undefined);
71+
engine.expect(weekEnum.findBy('label', 'weekday.monday')).toBe(weekEnum.items[1]);
72+
engine.expect(weekEnum.findBy('label', 'weekday.saturday')).toBe(weekEnum.items[6]);
73+
engine.expect(weekEnum.findBy('label', 'Invalid Label')).toBe(undefined);
74+
engine.expect(compactWeekEnum.findBy('key', 'Monday')).toBe(compactWeekEnum.items[1]);
75+
engine.expect(compactWeekEnum.findBy('key', 'Invalid Key')).toBe(undefined);
76+
engine.expect(compactWeekEnum.findBy('value', 1)).toBe(undefined);
77+
engine.expect(compactWeekEnum.findBy('value', 99)).toBe(undefined);
78+
engine.expect(compactWeekEnum.findBy('label', 'weekday.monday')).toBe(undefined);
79+
80+
// Custom meta field
81+
engine.expect(weekEnum.findBy('status', 'error')).toEqual(weekEnum.items[0]);
82+
engine.expect(weekEnum.findBy('status', 'warning')).toEqual(weekEnum.items[1]);
83+
engine.expect(weekEnum.findBy('status', 'success')).toEqual(weekEnum.items[3]);
84+
engine.expect(weekEnum.findBy('status', 'invalid')).toEqual(undefined);
85+
engine.expect(compactWeekEnum.findBy('status' as 'key', 'success')).toEqual(undefined);
86+
}
87+
);
88+
5789
engine.test(
5890
'enums.toList should generate an object array',
5991
({ EnumPlus: { Enum }, WeekConfig: { StandardWeekConfig, locales } }) => {

0 commit comments

Comments
 (0)