@naturalcycles/js-lib 15.87.0 → 15.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,32 @@
1
- import type { AnyObject, NumberEnum, StringEnum } from './types.js';
1
+ import type { AnyObject, Enum, NumberEnum, StringEnum } from './types.js';
2
+ /**
3
+ * Creates a runtime Enum object. A replacement for a native TypeScript enum,
4
+ * which is not supported by type-stripping runtimes (e.g `node --experimental-strip-types`).
5
+ *
6
+ * Declare the type with the same name as the const, to mimic how native enums merge value and type.
7
+ *
8
+ * @example
9
+ * export const Color = _enum({ RED: 'red', GREEN: 'green' })
10
+ * export type Color = typeof Color.type // 'red' | 'green'
11
+ *
12
+ * Color.RED // 'red'
13
+ * Color.keys // ['RED', 'GREEN']
14
+ * Color.values // ['red', 'green']
15
+ * Color.entries // [['RED', 'red'], ['GREEN', 'green']]
16
+ * Color.is(x) // type-guard for 'red' | 'green'
17
+ * Color.keyOf('red') // 'RED'
18
+ * JSON.stringify(Color) // '{"RED":"red","GREEN":"green"}', no reverse-mapping, no helper leakage
19
+ *
20
+ * The result doesn't satisfy `StringEnum`/`NumberEnum`, so the `_stringEnum*`/`_numberEnum*`
21
+ * functions don't accept it. Use the helpers on the returned object instead.
22
+ *
23
+ * Nothing is frozen, immutability is expressed through `readonly` types only.
24
+ *
25
+ * Throws if the input contains a key that collides with one of the helper names.
26
+ *
27
+ * @experimental
28
+ */
29
+ export declare function _enum<const T extends Record<string, string> | Record<string, number>>(obj: T): EnumObject<T>;
2
30
  export declare function getEnumType(en: AnyObject): 'StringEnum' | 'NumberEnum' | undefined;
3
31
  /**
4
32
  * Returns all String keys of a number-enum.
@@ -91,3 +119,51 @@ export declare function _numberEnumKeyOrUndefined<T extends NumberEnum>(en: T, v
91
119
  export declare function _numberEnumKey<T extends NumberEnum>(en: T, v: T[keyof T] | undefined | null): keyof T;
92
120
  export declare function _stringEnumKeyOrUndefined<T extends StringEnum>(en: T, v: string | undefined | null): keyof T | undefined;
93
121
  export declare function _stringEnumKey<T extends StringEnum>(en: T, v: string | undefined | null): keyof T;
122
+ /**
123
+ * @experimental
124
+ */
125
+ export type EnumObject<T> = Readonly<T> & EnumHelpers<T>;
126
+ /**
127
+ * Helpers that `_enum()` hangs off the returned object.
128
+ * Defined as non-enumerable, so that `Object.keys/values/entries`, `JSON.stringify`
129
+ * and `for..in` still only see the actual Enum members.
130
+ *
131
+ * `keys`/`values`/`entries` are precomputed, and keep the literal types
132
+ * that the `Object.*` equivalents would widen to `string`.
133
+ *
134
+ * @experimental
135
+ */
136
+ interface EnumHelpers<T> {
137
+ /**
138
+ * Type-only carrier for the union of all values, so that the Enum's type can be declared
139
+ * without a second import: `export type Color = typeof Color.type`.
140
+ *
141
+ * It has no runtime counterpart, reading it as a value returns undefined.
142
+ */
143
+ readonly type: Enum<T>;
144
+ readonly keys: readonly (keyof T)[];
145
+ readonly values: readonly Enum<T>[];
146
+ readonly entries: EnumEntries<T>;
147
+ /**
148
+ * Type-guard. O(1), backed by a Map.
149
+ */
150
+ readonly is: (v: unknown) => v is Enum<T>;
151
+ /**
152
+ * Reverse lookup, the counterpart of `Color.RED`. O(1). Throws if the value is not a member.
153
+ */
154
+ readonly keyOf: (v: Enum<T>) => keyof T;
155
+ /**
156
+ * Like `keyOf`, but takes untrusted input and returns undefined instead of throwing.
157
+ */
158
+ readonly keyOfOrUndefined: (v: unknown) => keyof T | undefined;
159
+ }
160
+ /**
161
+ * The mapped type (rather than `readonly [keyof T, Enum<T>][]`) keeps each key paired
162
+ * with its own value, instead of allowing any key/value combination.
163
+ *
164
+ * @experimental
165
+ */
166
+ type EnumEntries<T> = readonly {
167
+ [K in keyof T]: readonly [k: K, v: T[K]];
168
+ }[keyof T][];
169
+ export {};
package/dist/enum.util.js CHANGED
@@ -1,3 +1,68 @@
1
+ /**
2
+ * Creates a runtime Enum object. A replacement for a native TypeScript enum,
3
+ * which is not supported by type-stripping runtimes (e.g `node --experimental-strip-types`).
4
+ *
5
+ * Declare the type with the same name as the const, to mimic how native enums merge value and type.
6
+ *
7
+ * @example
8
+ * export const Color = _enum({ RED: 'red', GREEN: 'green' })
9
+ * export type Color = typeof Color.type // 'red' | 'green'
10
+ *
11
+ * Color.RED // 'red'
12
+ * Color.keys // ['RED', 'GREEN']
13
+ * Color.values // ['red', 'green']
14
+ * Color.entries // [['RED', 'red'], ['GREEN', 'green']]
15
+ * Color.is(x) // type-guard for 'red' | 'green'
16
+ * Color.keyOf('red') // 'RED'
17
+ * JSON.stringify(Color) // '{"RED":"red","GREEN":"green"}', no reverse-mapping, no helper leakage
18
+ *
19
+ * The result doesn't satisfy `StringEnum`/`NumberEnum`, so the `_stringEnum*`/`_numberEnum*`
20
+ * functions don't accept it. Use the helpers on the returned object instead.
21
+ *
22
+ * Nothing is frozen, immutability is expressed through `readonly` types only.
23
+ *
24
+ * Throws if the input contains a key that collides with one of the helper names.
25
+ *
26
+ * @experimental
27
+ */
28
+ export function _enum(obj) {
29
+ if (RESERVED_KEYS.some(k => Object.hasOwn(obj, k))) {
30
+ throw new Error(`_enum keys must not be named ${RESERVED_KEYS.join(', ')}, as they collide with the helpers`);
31
+ }
32
+ const rawEntries = Object.entries(obj);
33
+ const keys = Object.keys(obj);
34
+ const values = Object.values(obj);
35
+ const entries = rawEntries;
36
+ // Backs both `is` and `keyOf`. On duplicate values the last key wins, like a native number enum.
37
+ const keyByValue = new Map(rawEntries.map(([k, v]) => [v, k]));
38
+ return Object.defineProperties({ ...obj }, {
39
+ keys: { value: keys },
40
+ values: { value: values },
41
+ entries: { value: entries },
42
+ is: { value: (v) => keyByValue.has(v) },
43
+ keyOf: {
44
+ value: (v) => {
45
+ const key = keyByValue.get(v);
46
+ if (key === undefined)
47
+ throw new Error(`_enum keyOf not found for: ${String(v)}`);
48
+ return key;
49
+ },
50
+ },
51
+ keyOfOrUndefined: { value: (v) => keyByValue.get(v) },
52
+ });
53
+ }
54
+ /**
55
+ * Keys that `_enum()` reserves for its helpers, and therefore rejects as Enum member names.
56
+ */
57
+ const RESERVED_KEYS = [
58
+ 'type',
59
+ 'keys',
60
+ 'values',
61
+ 'entries',
62
+ 'is',
63
+ 'keyOf',
64
+ 'keyOfOrUndefined',
65
+ ];
1
66
  export function getEnumType(en) {
2
67
  /*
3
68
  * enum Foo { A = 1, B = 2 }
package/dist/types.d.ts CHANGED
@@ -28,6 +28,20 @@ export type AnyObject = Record<string, any>;
28
28
  export type AnyEnum = NumberEnum;
29
29
  export type NumberEnum = Record<string, number | string>;
30
30
  export type StringEnum = Record<string, string>;
31
+ /**
32
+ * Union of all values of an Enum-like object: both native TypeScript enums and `_enum()` objects.
33
+ *
34
+ * For an `_enum()` object prefer `typeof Color.type`, which needs no import.
35
+ *
36
+ * @example
37
+ * enum Color { RED = 'red', GREEN = 'green' }
38
+ * type ColorValue = Enum<typeof Color> // 'red' | 'green'
39
+ *
40
+ * @experimental
41
+ */
42
+ export type Enum<T> = T extends {
43
+ values: readonly (infer V)[];
44
+ } ? V : T[keyof T];
31
45
  export type CreatedUpdated = {
32
46
  created: UnixTimestamp;
33
47
  updated: UnixTimestamp;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.87.0",
4
+ "version": "15.88.0",
5
5
  "dependencies": {
6
6
  "tslib": "^2"
7
7
  },
package/src/enum.util.ts CHANGED
@@ -1,4 +1,79 @@
1
- import type { AnyObject, NumberEnum, StringEnum } from './types.js'
1
+ import type { AnyObject, Enum, NumberEnum, StringEnum } from './types.js'
2
+
3
+ /**
4
+ * Creates a runtime Enum object. A replacement for a native TypeScript enum,
5
+ * which is not supported by type-stripping runtimes (e.g `node --experimental-strip-types`).
6
+ *
7
+ * Declare the type with the same name as the const, to mimic how native enums merge value and type.
8
+ *
9
+ * @example
10
+ * export const Color = _enum({ RED: 'red', GREEN: 'green' })
11
+ * export type Color = typeof Color.type // 'red' | 'green'
12
+ *
13
+ * Color.RED // 'red'
14
+ * Color.keys // ['RED', 'GREEN']
15
+ * Color.values // ['red', 'green']
16
+ * Color.entries // [['RED', 'red'], ['GREEN', 'green']]
17
+ * Color.is(x) // type-guard for 'red' | 'green'
18
+ * Color.keyOf('red') // 'RED'
19
+ * JSON.stringify(Color) // '{"RED":"red","GREEN":"green"}', no reverse-mapping, no helper leakage
20
+ *
21
+ * The result doesn't satisfy `StringEnum`/`NumberEnum`, so the `_stringEnum*`/`_numberEnum*`
22
+ * functions don't accept it. Use the helpers on the returned object instead.
23
+ *
24
+ * Nothing is frozen, immutability is expressed through `readonly` types only.
25
+ *
26
+ * Throws if the input contains a key that collides with one of the helper names.
27
+ *
28
+ * @experimental
29
+ */
30
+ export function _enum<const T extends Record<string, string> | Record<string, number>>(
31
+ obj: T,
32
+ ): EnumObject<T> {
33
+ if (RESERVED_KEYS.some(k => Object.hasOwn(obj, k))) {
34
+ throw new Error(
35
+ `_enum keys must not be named ${RESERVED_KEYS.join(', ')}, as they collide with the helpers`,
36
+ )
37
+ }
38
+
39
+ const rawEntries = Object.entries(obj)
40
+ const keys = Object.keys(obj) as (keyof T)[]
41
+ const values = Object.values(obj) as Enum<T>[]
42
+ const entries = rawEntries as unknown as EnumEntries<T>
43
+ // Backs both `is` and `keyOf`. On duplicate values the last key wins, like a native number enum.
44
+ const keyByValue = new Map<unknown, keyof T>(rawEntries.map(([k, v]) => [v, k]))
45
+
46
+ return Object.defineProperties(
47
+ { ...obj },
48
+ {
49
+ keys: { value: keys },
50
+ values: { value: values },
51
+ entries: { value: entries },
52
+ is: { value: (v: unknown): v is Enum<T> => keyByValue.has(v) },
53
+ keyOf: {
54
+ value: (v: Enum<T>): keyof T => {
55
+ const key = keyByValue.get(v)
56
+ if (key === undefined) throw new Error(`_enum keyOf not found for: ${String(v)}`)
57
+ return key
58
+ },
59
+ },
60
+ keyOfOrUndefined: { value: (v: unknown): keyof T | undefined => keyByValue.get(v) },
61
+ },
62
+ ) as EnumObject<T>
63
+ }
64
+
65
+ /**
66
+ * Keys that `_enum()` reserves for its helpers, and therefore rejects as Enum member names.
67
+ */
68
+ const RESERVED_KEYS = [
69
+ 'type',
70
+ 'keys',
71
+ 'values',
72
+ 'entries',
73
+ 'is',
74
+ 'keyOf',
75
+ 'keyOfOrUndefined',
76
+ ] as const
2
77
 
3
78
  export function getEnumType(en: AnyObject): 'StringEnum' | 'NumberEnum' | undefined {
4
79
  /*
@@ -226,3 +301,51 @@ export function _stringEnumKey<T extends StringEnum>(en: T, v: string | undefine
226
301
  if (!r) throw new Error(`_stringEnumKey not found for: ${v}`)
227
302
  return r
228
303
  }
304
+
305
+ /**
306
+ * @experimental
307
+ */
308
+ export type EnumObject<T> = Readonly<T> & EnumHelpers<T>
309
+
310
+ /**
311
+ * Helpers that `_enum()` hangs off the returned object.
312
+ * Defined as non-enumerable, so that `Object.keys/values/entries`, `JSON.stringify`
313
+ * and `for..in` still only see the actual Enum members.
314
+ *
315
+ * `keys`/`values`/`entries` are precomputed, and keep the literal types
316
+ * that the `Object.*` equivalents would widen to `string`.
317
+ *
318
+ * @experimental
319
+ */
320
+ interface EnumHelpers<T> {
321
+ /**
322
+ * Type-only carrier for the union of all values, so that the Enum's type can be declared
323
+ * without a second import: `export type Color = typeof Color.type`.
324
+ *
325
+ * It has no runtime counterpart, reading it as a value returns undefined.
326
+ */
327
+ readonly type: Enum<T>
328
+ readonly keys: readonly (keyof T)[]
329
+ readonly values: readonly Enum<T>[]
330
+ readonly entries: EnumEntries<T>
331
+ /**
332
+ * Type-guard. O(1), backed by a Map.
333
+ */
334
+ readonly is: (v: unknown) => v is Enum<T>
335
+ /**
336
+ * Reverse lookup, the counterpart of `Color.RED`. O(1). Throws if the value is not a member.
337
+ */
338
+ readonly keyOf: (v: Enum<T>) => keyof T
339
+ /**
340
+ * Like `keyOf`, but takes untrusted input and returns undefined instead of throwing.
341
+ */
342
+ readonly keyOfOrUndefined: (v: unknown) => keyof T | undefined
343
+ }
344
+
345
+ /**
346
+ * The mapped type (rather than `readonly [keyof T, Enum<T>][]`) keeps each key paired
347
+ * with its own value, instead of allowing any key/value combination.
348
+ *
349
+ * @experimental
350
+ */
351
+ type EnumEntries<T> = readonly { [K in keyof T]: readonly [k: K, v: T[K]] }[keyof T][]
package/src/types.ts CHANGED
@@ -34,6 +34,21 @@ export type AnyEnum = NumberEnum
34
34
  export type NumberEnum = Record<string, number | string>
35
35
  export type StringEnum = Record<string, string>
36
36
 
37
+ /**
38
+ * Union of all values of an Enum-like object: both native TypeScript enums and `_enum()` objects.
39
+ *
40
+ * For an `_enum()` object prefer `typeof Color.type`, which needs no import.
41
+ *
42
+ * @example
43
+ * enum Color { RED = 'red', GREEN = 'green' }
44
+ * type ColorValue = Enum<typeof Color> // 'red' | 'green'
45
+ *
46
+ * @experimental
47
+ */
48
+ // The first branch is for `_enum()` objects, whose helpers `T[keyof T]` would otherwise pull into
49
+ // the union. Native enums never match it, since their values are `string | number`, never an array.
50
+ export type Enum<T> = T extends { values: readonly (infer V)[] } ? V : T[keyof T]
51
+
37
52
  // oxlint-disable-next-line @typescript-eslint/consistent-type-definitions
38
53
  export type CreatedUpdated = {
39
54
  created: UnixTimestamp