Skip to content

Commit 3ef6f0b

Browse files
committed
feat: enhance enum handling with functional labels
1 parent 4887283 commit 3ef6f0b

File tree

11 files changed

+134
-50
lines changed

11 files changed

+134
-50
lines changed

CHANGELOG.md

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

33
# enum-plus Changelog
44

5+
## 3.1.5
6+
7+
2025-11-20
8+
9+
### Features
10+
11+
- ✨ Add `AnyEnum` type as a generic enum type that can be used to represent any enum collection.
12+
13+
### Bug Fixes
14+
15+
- 🐞 Fix the issue where `enum.meta` is always empty in `meta-only` mode.
16+
517
## 3.1.4
618

719
2025-11-19

src/enum-collection.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ export class EnumCollectionClass<
127127
* set.
128128
*/
129129
get name(): string | undefined {
130+
if (typeof this.__options__?.name === 'function') {
131+
return this.__options__.name(undefined!);
132+
}
130133
const localize = this.__options__?.localize ?? localizer.localize;
131134
if (typeof localize === 'function') {
132135
return localize(this.__options__?.name);

src/enum-item.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { internalConfig, localizer } from './global-config';
2-
import type { EnumItemInit, EnumKey, EnumValue, LocalizeInterface, ValueTypeFromSingleInit } from './types';
2+
import type {
3+
EnumItemInit,
4+
EnumItemLabel,
5+
EnumKey,
6+
EnumValue,
7+
LocalizeInterface,
8+
ValueTypeFromSingleInit,
9+
} from './types';
310
import { IS_ENUM_ITEM } from './utils';
411

512
/**
@@ -20,7 +27,7 @@ export class EnumItemClass<
2027
const P = any,
2128
> {
2229
private _options: EnumItemOptions<T, K, V, P> | undefined;
23-
private _label: string | undefined;
30+
private _label: EnumItemLabel | undefined;
2431
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2532
private _localize: (content: string | undefined) => any;
2633

@@ -34,10 +41,10 @@ export class EnumItemClass<
3441
* @param raw The original initialization object | 原始初始化对象
3542
* @param options Optional settings for the enum item | 枚举项的可选设置
3643
*/
37-
constructor(key: K, value: V, label: string, raw: T, options?: EnumItemOptions<T, K, V, P>) {
44+
constructor(key: K, value: V, label: EnumItemLabel, raw: T, options?: EnumItemOptions<T, K, V, P>) {
3845
this.key = key;
3946
this.value = value;
40-
this.label = label;
47+
this.label = label as string;
4148
this.raw = raw;
4249

4350
// Should use _label instead of label closure, to make sure it can be serialized correctly
@@ -62,6 +69,10 @@ export class EnumItemClass<
6269
const labelPrefix = this._options?.labelPrefix;
6370
const autoLabel = this._options?.autoLabel ?? internalConfig.autoLabel;
6471
let localeKey = this._label;
72+
if (typeof localeKey === 'function') {
73+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
74+
return localeKey(this as any);
75+
}
6576
if (autoLabel && labelPrefix != null) {
6677
if (typeof autoLabel === 'function') {
6778
localeKey = autoLabel({

src/enum-items.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { EnumItemClass } from './enum-item';
33
import type {
44
EnumInit,
55
EnumItemInit,
6+
EnumItemLabel,
67
EnumKey,
78
EnumValue,
89
ExactEqual,
@@ -107,7 +108,7 @@ export class EnumItemsArray<
107108
Object.keys(itemRaw).forEach((k) => {
108109
const metaKey = k as Exclude<keyof T[keyof T], 'key' | 'value' | 'label'>;
109110
if (metaKey !== 'key' && metaKey !== 'value' && metaKey !== 'label') {
110-
if (!meta[metaKey]) {
111+
if (meta[metaKey] == null) {
111112
meta[metaKey] = [];
112113
}
113114
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -865,42 +866,42 @@ function parseEnumItem<
865866
V extends EnumValue,
866867
>(init: T, key: K): StandardEnumItemInit<V> {
867868
let value: V;
868-
let label: string;
869+
let label: EnumItemLabel;
869870
if (init != null) {
870871
if (typeof init === 'number' || typeof init === 'string' || typeof init === 'symbol') {
871872
value = init as V;
872-
label = key as string;
873+
label = key as EnumItemLabel;
873874
} else if (typeof init === 'object') {
874875
// Initialize using object
875876
if (Object.prototype.toString.call(init) === '[object Object]') {
876877
if ('value' in init && Object.keys(init).some((k) => k === 'value')) {
877878
// type of {value, label}
878879
value = (init.value ?? key) as V;
879880
if ('label' in init && Object.keys(init).some((k) => k === 'label')) {
880-
label = init.label as string;
881+
label = init.label!;
881882
} else {
882-
label = key as string;
883+
label = key as EnumItemLabel;
883884
}
884885
} else if ('label' in init && Object.keys(init).some((k) => k === 'label')) {
885886
// typeof {label}
886887
value = key as unknown as V;
887-
label = init.label ?? (key as string);
888+
label = init.label ?? (key as EnumItemLabel);
888889
} else {
889890
// {} empty object
890891
value = key as unknown as V;
891-
label = key as string;
892+
label = key as EnumItemLabel;
892893
}
893894
} else {
894895
// Probably Date, RegExp and other primitive types
895896
value = init as V;
896-
label = key as string;
897+
label = key as EnumItemLabel;
897898
}
898899
} else {
899900
throw new Error(`Invalid enum item: ${JSON.stringify(init)}`);
900901
}
901902
} else {
902903
value = key as unknown as V;
903-
label = key as string;
904+
label = key as EnumItemLabel;
904905
}
905906
return { value, label };
906907
}

src/extension.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ declare module 'enum-plus/extension' {
2424
*
2525
* **CN:** 枚举本地化文本的Key值,可以用来增强编辑器的智能提示
2626
*/
27-
LocaleKeys: NonNullable<string>;
27+
LocaleKeys: // eslint-disable-next-line @typescript-eslint/ban-types
28+
| (string & {})
29+
| ((
30+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
31+
item: import('./enum-item').EnumItemClass<import('./types').StandardEnumItemInit<import('./types').EnumValue>>
32+
) => string | undefined);
2833
}
2934
}

src/localize-interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { EnumLocaleExtends } from 'enum-plus/extension';
22

33
export type LocalizeInterface = (
4-
localeKey: EnumLocaleExtends['LocaleKeys'] | undefined
4+
localeKey: Exclude<EnumLocaleExtends['LocaleKeys'], (...args: any[]) => string | undefined> | undefined
55
// eslint-disable-next-line @typescript-eslint/no-explicit-any
66
) => any;

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,6 @@ export const IS_ENUM_ITEMS = Symbol.for('[IsEnumItems]');
7474
* English, does not provide actual localization functions
7575
* - **CN:** 默认的全局本地化函数,仅用于将内置资源解析为英文,并不提供实际的本地化功能
7676
*/
77-
export const defaultLocalize: LocalizeInterface = (content) => {
77+
export const defaultLocalize: LocalizeInterface = (content): string | undefined => {
7878
return content;
7979
};

test/data/week-config.ts

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { defaultLocalize as defaultLocalizeType, Enum as EnumType } from '@enum-plus';
1+
import type { defaultLocalize as defaultLocalizeType, EnumItemClass, Enum as EnumType } from '@enum-plus';
2+
import type { EnumValue, StandardEnumItemInit } from '@enum-plus/types';
23

34
export const localeEN = {
45
'weekDay.name': 'Week Days',
@@ -48,10 +49,12 @@ export let locales: typeof localeEN | typeof localeCN | typeof noLocale = noLoca
4849
export let lang: 'en-US' | 'zh-CN' | undefined = undefined;
4950

5051
export function getLocales(language: typeof lang) {
51-
return language === 'zh-CN' ? getLocales.localeCN : language ? getLocales.localeEN : noLocale;
52+
const { localeCN, localeEN, noLocale } = getLocales;
53+
return language === 'zh-CN' ? localeCN : language ? localeEN : noLocale;
5254
}
5355
getLocales.localeCN = localeCN;
5456
getLocales.localeEN = localeEN;
57+
getLocales.noLocale = noLocale;
5558

5659
type getLocalesType = typeof getLocales;
5760
export function setLang(
@@ -60,9 +63,10 @@ export function setLang(
6063
getLocales: getLocalesType,
6164
defaultLocalize: typeof defaultLocalizeType
6265
) {
66+
const { genSillyLocalizer } = setLang;
6367
lang = value;
6468
locales = getLocales(value);
65-
Enum.localize = value ? setLang.genSillyLocalizer(value, getLocales, defaultLocalize) : defaultLocalize;
69+
Enum.localize = value ? genSillyLocalizer(value, getLocales) : defaultLocalize;
6670
}
6771
setLang.genSillyLocalizer = genSillyLocalizer;
6872

@@ -81,6 +85,15 @@ export const StandardWeekConfig = {
8185
Friday: { value: 5, label: noLocale.Friday, status: 'success' },
8286
Saturday: { value: 6, label: noLocale.Saturday, status: 'error' },
8387
} as const;
88+
export const FuncLabelStandardWeekConfig = {
89+
Sunday: { value: 0, label: labelLocalizer },
90+
Monday: { value: 1, label: labelLocalizer },
91+
Tuesday: { value: 2, label: labelLocalizer },
92+
Wednesday: { value: 3, label: labelLocalizer },
93+
Thursday: { value: 4, label: labelLocalizer },
94+
Friday: { value: 5, label: labelLocalizer },
95+
Saturday: { value: 6, label: labelLocalizer },
96+
} as const;
8497
export const ShortLabelStandardWeekConfig = {
8598
Sunday: { value: 0, label: 'Sunday' },
8699
Monday: { value: 1, label: 'Monday' },
@@ -180,41 +193,38 @@ export const WeekMetaOnlyConfig = Object.keys(StandardWeekConfig).reduce(
180193
{} as { [key in TKey]: Omit<TConfig[key], 'value' | 'label'> }
181194
);
182195

183-
export function genSillyLocalizer(
184-
language: typeof lang,
185-
getLocales: getLocalesType,
186-
defaultLocalize: typeof defaultLocalizeType
187-
) {
196+
export function genSillyLocalizer(language: typeof lang, getLocales: getLocalesType) {
188197
// should use function here to avoid closure. this is important for the e2e test cases.
189198
function sillyLocalize(
190-
content: (typeof StandardWeekConfig)[keyof typeof StandardWeekConfig]['label'] | NonNullable<string> | undefined
191-
): typeof content {
192-
const locales = sillyLocalize.locales;
199+
// eslint-disable-next-line @typescript-eslint/ban-types
200+
content: (typeof noLocale)[keyof typeof noLocale] | (string & {}) | undefined
201+
): string | undefined {
202+
const { locales } = sillyLocalize;
193203
switch (content) {
194204
case 'weekDay.name':
195-
return locales['weekDay.name'] as typeof content;
205+
return locales['weekDay.name'];
196206
case 'weekday.Sunday':
197-
return locales.Sunday as typeof content;
207+
return locales.Sunday;
198208
case 'weekday.Monday':
199-
return locales.Monday as typeof content;
209+
return locales.Monday;
200210
case 'weekday.Tuesday':
201-
return locales.Tuesday as typeof content;
211+
return locales.Tuesday;
202212
case 'weekday.Wednesday':
203-
return locales.Wednesday as typeof content;
213+
return locales.Wednesday;
204214
case 'weekday.Thursday':
205-
return locales.Thursday as typeof content;
215+
return locales.Thursday;
206216
case 'weekday.Friday':
207-
return locales.Friday as typeof content;
217+
return locales.Friday;
208218
case 'weekday.Saturday':
209-
return locales.Saturday as typeof content;
210-
case 'boolean.youes':
211-
return locales.Yes as typeof content;
219+
return locales.Saturday;
220+
case 'boolean.Yes':
221+
return locales.Yes;
212222
case 'boolean.No':
213-
return locales.No as typeof content;
223+
return locales.No;
214224
case 'date.FirstDay':
215-
return locales.FirstDay as typeof content;
225+
return locales.FirstDay;
216226
case 'date.LastDay':
217-
return locales.LastDay as typeof content;
227+
return locales.LastDay;
218228
default:
219229
return content;
220230
}
@@ -223,6 +233,16 @@ export function genSillyLocalizer(
223233
return sillyLocalize;
224234
}
225235

236+
// eslint-disable-next-line @typescript-eslint/ban-types
237+
export function labelLocalizer(item: EnumItemClass<StandardEnumItemInit<EnumValue>>) {
238+
const { genSillyLocalizer, getLocales, noLocale } = labelLocalizer;
239+
const localizer = genSillyLocalizer(lang, getLocales);
240+
return localizer(noLocale[item.key as keyof typeof noLocale]);
241+
}
242+
labelLocalizer.genSillyLocalizer = genSillyLocalizer;
243+
labelLocalizer.getLocales = getLocales;
244+
labelLocalizer.noLocale = noLocale;
245+
226246
export function localizeConfigData(
227247
config: typeof StandardWeekConfig,
228248
locales: typeof localeEN | typeof localeCN | typeof noLocale
@@ -239,7 +259,8 @@ export function localizeConfigData(
239259
defaultLocalize?: typeof defaultLocalizeType
240260
) {
241261
if (typeof getLocales === 'function' && defaultLocalize) {
242-
const localizer = localizeConfigData.genSillyLocalizer(lang, getLocales, defaultLocalize);
262+
const { genSillyLocalizer } = localizeConfigData;
263+
const localizer = genSillyLocalizer(lang, getLocales);
243264
return Object.keys(config).reduce(
244265
(acc, key) => {
245266
// @ts-expect-error: because cannot assign to 'value' because it is a read-only property.

test/test-suites/create-enum.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { toPlainEnums } from '../utils/index';
1515
const testCreatingEnum = (engine: TestEngineBase<'jest' | 'playwright'>) => {
1616
engine.describe('Create Enum using static init data', () => {
1717
engine.test(
18-
'Should be created with standard init data',
18+
'Should be created with standard init config',
1919
({ EnumPlus: { Enum }, WeekConfig: { StandardWeekConfig, locales } }) => {
2020
const weekEnum = Enum(StandardWeekConfig);
2121
return { weekEnum, locales };
@@ -24,7 +24,24 @@ const testCreatingEnum = (engine: TestEngineBase<'jest' | 'playwright'>) => {
2424
engine.expect(weekEnum).toBeDefined();
2525
engine.expect(weekEnum).not.toBeNull();
2626
engine.expect(weekEnum?.items?.length).toBe(7);
27-
engine.expect(toPlainEnums(weekEnum.items)).toEqual(getStandardWeekData(locales));
27+
engine.expect(toPlainEnums(weekEnum?.items)).toEqual(getStandardWeekData(locales));
28+
}
29+
);
30+
engine.test(
31+
'Should be created with functional label init config',
32+
({ EnumPlus: { Enum }, WeekConfig: { FuncLabelStandardWeekConfig, locales } }) => {
33+
const weekEnum = Enum(FuncLabelStandardWeekConfig);
34+
return {
35+
weekEnum,
36+
items: Array.from(weekEnum.items).map(({ key, value, label }) => ({ key, value, label })),
37+
locales,
38+
};
39+
},
40+
({ weekEnum, items, locales }) => {
41+
engine.expect(weekEnum).toBeDefined();
42+
engine.expect(weekEnum).not.toBeNull();
43+
engine.expect(items.length).toBe(7);
44+
engine.expect(items).toEqual(getStandardWeekData(locales));
2845
}
2946
);
3047

0 commit comments

Comments
 (0)