@digitaldefiance/branded-enum 0.0.1

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.
@@ -0,0 +1,1589 @@
1
+ /**
2
+ * Advanced enum operations for branded enums.
3
+ *
4
+ * Provides functions for deriving new enums from existing ones,
5
+ * including subsetting, exclusion, and transformation operations.
6
+ */ import { ENUM_ID, ENUM_VALUES } from './types.js';
7
+ import { createBrandedEnum } from './factory.js';
8
+ /**
9
+ * Checks if an object is a branded enum (has Symbol metadata).
10
+ *
11
+ * @param obj - The object to check
12
+ * @returns true if obj is a branded enum
13
+ */ function isBrandedEnum(obj) {
14
+ return obj !== null && typeof obj === 'object' && ENUM_ID in obj && ENUM_VALUES in obj && typeof obj[ENUM_ID] === 'string' && obj[ENUM_VALUES] instanceof Set;
15
+ }
16
+ /**
17
+ * Creates a new branded enum containing only the specified keys from the source enum.
18
+ *
19
+ * This function derives a subset of an existing branded enum by selecting specific keys.
20
+ * The resulting enum is registered as an independent enum in the global registry.
21
+ *
22
+ * Type safety is maintained - the resulting enum's type reflects only the selected keys.
23
+ *
24
+ * @template E - The source branded enum type
25
+ * @template K - The keys to include in the subset (must be keys of E)
26
+ * @param newId - Unique identifier for the new subset enum. Must not already be registered.
27
+ * @param sourceEnum - The branded enum to derive the subset from
28
+ * @param keys - Array of keys to include in the subset. All keys must exist in sourceEnum.
29
+ * @returns A new branded enum containing only the specified key-value pairs
30
+ * @throws {Error} Throws `Error` with message `enumSubset requires a branded enum as the source`
31
+ * if sourceEnum is not a valid branded enum.
32
+ * @throws {Error} Throws `Error` with message `enumSubset requires at least one key`
33
+ * if keys array is empty.
34
+ * @throws {Error} Throws `Error` with message `Key "${key}" does not exist in enum "${enumId}"`
35
+ * if any specified key does not exist in the source enum.
36
+ * @throws {Error} Throws `Error` with message `Branded enum with ID "${newId}" already exists`
37
+ * if newId is already registered.
38
+ *
39
+ * @example
40
+ * // Basic usage - create a subset of colors
41
+ * const AllColors = createBrandedEnum('all-colors', {
42
+ * Red: 'red',
43
+ * Green: 'green',
44
+ * Blue: 'blue',
45
+ * Yellow: 'yellow',
46
+ * } as const);
47
+ *
48
+ * const PrimaryColors = enumSubset('primary-colors', AllColors, ['Red', 'Blue', 'Yellow']);
49
+ * // PrimaryColors has: Red, Blue, Yellow (no Green)
50
+ *
51
+ * PrimaryColors.Red; // 'red'
52
+ * PrimaryColors.Blue; // 'blue'
53
+ * // PrimaryColors.Green; // TypeScript error - Green doesn't exist
54
+ *
55
+ * @example
56
+ * // Type safety with subset
57
+ * const Status = createBrandedEnum('status', {
58
+ * Active: 'active',
59
+ * Inactive: 'inactive',
60
+ * Pending: 'pending',
61
+ * Archived: 'archived',
62
+ * } as const);
63
+ *
64
+ * const ActiveStatuses = enumSubset('active-statuses', Status, ['Active', 'Pending']);
65
+ * type ActiveStatusValue = typeof ActiveStatuses[keyof typeof ActiveStatuses];
66
+ * // ActiveStatusValue = 'active' | 'pending'
67
+ *
68
+ * @example
69
+ * // Error handling for invalid keys
70
+ * try {
71
+ * enumSubset('invalid', AllColors, ['Red', 'Purple']); // Purple doesn't exist
72
+ * } catch (e) {
73
+ * console.log(e.message);
74
+ * // 'Key "Purple" does not exist in enum "all-colors"'
75
+ * }
76
+ */ export function enumSubset(newId, sourceEnum, keys) {
77
+ // Validate that sourceEnum is a branded enum
78
+ if (!isBrandedEnum(sourceEnum)) {
79
+ throw new Error('enumSubset requires a branded enum as the source');
80
+ }
81
+ // Validate that keys array is not empty
82
+ if (keys.length === 0) {
83
+ throw new Error('enumSubset requires at least one key');
84
+ }
85
+ const sourceEnumId = sourceEnum[ENUM_ID];
86
+ // Build the subset values object
87
+ const subsetValues = {};
88
+ for (const key of keys){
89
+ // Validate that the key exists in the source enum
90
+ if (!(key in sourceEnum)) {
91
+ throw new Error(`Key "${key}" does not exist in enum "${sourceEnumId}"`);
92
+ }
93
+ // Copy the key-value pair
94
+ subsetValues[key] = sourceEnum[key];
95
+ }
96
+ // Create and return the new branded enum (this handles registration)
97
+ return createBrandedEnum(newId, subsetValues);
98
+ }
99
+ /**
100
+ * Creates a new branded enum by excluding the specified keys from the source enum.
101
+ *
102
+ * This function derives a new enum from an existing branded enum by removing specific keys.
103
+ * It is the complement of `enumSubset` - instead of specifying which keys to include,
104
+ * you specify which keys to exclude.
105
+ *
106
+ * The resulting enum is registered as an independent enum in the global registry.
107
+ * Type safety is maintained - the resulting enum's type reflects only the remaining keys.
108
+ *
109
+ * @template E - The source branded enum type
110
+ * @template K - The keys to exclude from the result (must be keys of E)
111
+ * @param newId - Unique identifier for the new enum. Must not already be registered.
112
+ * @param sourceEnum - The branded enum to derive from
113
+ * @param keysToExclude - Array of keys to exclude. All keys must exist in sourceEnum.
114
+ * @returns A new branded enum containing all key-value pairs except the excluded ones
115
+ * @throws {Error} Throws `Error` with message `enumExclude requires a branded enum as the source`
116
+ * if sourceEnum is not a valid branded enum.
117
+ * @throws {Error} Throws `Error` with message `enumExclude: excluding all keys would result in an empty enum`
118
+ * if excluding all keys would leave no keys remaining.
119
+ * @throws {Error} Throws `Error` with message `Key "${key}" does not exist in enum "${enumId}"`
120
+ * if any specified key to exclude does not exist in the source enum.
121
+ * @throws {Error} Throws `Error` with message `Branded enum with ID "${newId}" already exists`
122
+ * if newId is already registered.
123
+ *
124
+ * @example
125
+ * // Basic usage - exclude specific colors
126
+ * const AllColors = createBrandedEnum('all-colors', {
127
+ * Red: 'red',
128
+ * Green: 'green',
129
+ * Blue: 'blue',
130
+ * Yellow: 'yellow',
131
+ * } as const);
132
+ *
133
+ * const NonPrimaryColors = enumExclude('non-primary', AllColors, ['Red', 'Blue', 'Yellow']);
134
+ * // NonPrimaryColors has only: Green
135
+ *
136
+ * NonPrimaryColors.Green; // 'green'
137
+ * // NonPrimaryColors.Red; // TypeScript error - Red was excluded
138
+ *
139
+ * @example
140
+ * // Exclude deprecated values
141
+ * const Status = createBrandedEnum('status', {
142
+ * Active: 'active',
143
+ * Inactive: 'inactive',
144
+ * Pending: 'pending',
145
+ * Deprecated: 'deprecated',
146
+ * } as const);
147
+ *
148
+ * const CurrentStatuses = enumExclude('current-statuses', Status, ['Deprecated']);
149
+ * type CurrentStatusValue = typeof CurrentStatuses[keyof typeof CurrentStatuses];
150
+ * // CurrentStatusValue = 'active' | 'inactive' | 'pending'
151
+ *
152
+ * @example
153
+ * // Error handling for invalid keys
154
+ * try {
155
+ * enumExclude('invalid', AllColors, ['Purple']); // Purple doesn't exist
156
+ * } catch (e) {
157
+ * console.log(e.message);
158
+ * // 'Key "Purple" does not exist in enum "all-colors"'
159
+ * }
160
+ */ export function enumExclude(newId, sourceEnum, keysToExclude) {
161
+ // Validate that sourceEnum is a branded enum
162
+ if (!isBrandedEnum(sourceEnum)) {
163
+ throw new Error('enumExclude requires a branded enum as the source');
164
+ }
165
+ const sourceEnumId = sourceEnum[ENUM_ID];
166
+ // Validate that all keys to exclude exist in the source enum
167
+ for (const key of keysToExclude){
168
+ if (!(key in sourceEnum)) {
169
+ throw new Error(`Key "${key}" does not exist in enum "${sourceEnumId}"`);
170
+ }
171
+ }
172
+ // Create a Set for O(1) lookup of excluded keys
173
+ const excludeSet = new Set(keysToExclude);
174
+ // Get all keys from the source enum (excluding Symbol metadata)
175
+ const allKeys = Object.keys(sourceEnum);
176
+ // Build the result values object with non-excluded keys
177
+ const resultValues = {};
178
+ for (const key of allKeys){
179
+ if (!excludeSet.has(key)) {
180
+ resultValues[key] = sourceEnum[key];
181
+ }
182
+ }
183
+ // Validate that we have at least one key remaining
184
+ if (Object.keys(resultValues).length === 0) {
185
+ throw new Error('enumExclude: excluding all keys would result in an empty enum');
186
+ }
187
+ // Create and return the new branded enum (this handles registration)
188
+ return createBrandedEnum(newId, resultValues);
189
+ }
190
+ /**
191
+ * Creates a new branded enum by transforming all values through a mapper function.
192
+ *
193
+ * This function derives a new enum from an existing branded enum by applying a
194
+ * transformation function to each value. The keys remain unchanged, but the values
195
+ * are transformed according to the provided mapper.
196
+ *
197
+ * Common use cases include:
198
+ * - Prefixing values (e.g., adding a namespace)
199
+ * - Suffixing values (e.g., adding a version)
200
+ * - Case transformation (e.g., uppercase, lowercase)
201
+ * - Custom transformations (e.g., encoding, formatting)
202
+ *
203
+ * The resulting enum is registered as an independent enum in the global registry.
204
+ *
205
+ * @template E - The source branded enum type
206
+ * @param newId - Unique identifier for the new enum. Must not already be registered.
207
+ * @param sourceEnum - The branded enum to derive from
208
+ * @param mapper - Function that transforms each value. Receives the original value
209
+ * and the key, and returns the transformed value.
210
+ * @returns A new branded enum with transformed values
211
+ * @throws {Error} Throws `Error` with message `enumMap requires a branded enum as the source`
212
+ * if sourceEnum is not a valid branded enum.
213
+ * @throws {Error} Throws `Error` with message `enumMap mapper must return a string`
214
+ * if the mapper function returns a non-string value.
215
+ * @throws {Error} Throws `Error` with message `Branded enum with ID "${newId}" already exists`
216
+ * if newId is already registered.
217
+ *
218
+ * @example
219
+ * // Prefix all values with a namespace
220
+ * const Status = createBrandedEnum('status', {
221
+ * Active: 'active',
222
+ * Inactive: 'inactive',
223
+ * } as const);
224
+ *
225
+ * const PrefixedStatus = enumMap('prefixed-status', Status, (value) => `app.${value}`);
226
+ * // PrefixedStatus.Active === 'app.active'
227
+ * // PrefixedStatus.Inactive === 'app.inactive'
228
+ *
229
+ * @example
230
+ * // Uppercase all values
231
+ * const Colors = createBrandedEnum('colors', {
232
+ * Red: 'red',
233
+ * Green: 'green',
234
+ * Blue: 'blue',
235
+ * } as const);
236
+ *
237
+ * const UpperColors = enumMap('upper-colors', Colors, (value) => value.toUpperCase());
238
+ * // UpperColors.Red === 'RED'
239
+ * // UpperColors.Green === 'GREEN'
240
+ * // UpperColors.Blue === 'BLUE'
241
+ *
242
+ * @example
243
+ * // Transform with key context
244
+ * const Sizes = createBrandedEnum('sizes', {
245
+ * Small: 's',
246
+ * Medium: 'm',
247
+ * Large: 'l',
248
+ * } as const);
249
+ *
250
+ * const VerboseSizes = enumMap('verbose-sizes', Sizes, (value, key) => `${key.toLowerCase()}-${value}`);
251
+ * // VerboseSizes.Small === 'small-s'
252
+ * // VerboseSizes.Medium === 'medium-m'
253
+ * // VerboseSizes.Large === 'large-l'
254
+ */ export function enumMap(newId, sourceEnum, mapper) {
255
+ // Validate that sourceEnum is a branded enum
256
+ if (!isBrandedEnum(sourceEnum)) {
257
+ throw new Error('enumMap requires a branded enum as the source');
258
+ }
259
+ // Get all keys from the source enum (excluding Symbol metadata)
260
+ const allKeys = Object.keys(sourceEnum);
261
+ // Build the result values object with transformed values
262
+ const resultValues = {};
263
+ for (const key of allKeys){
264
+ const originalValue = sourceEnum[key];
265
+ const transformedValue = mapper(originalValue, key);
266
+ // Validate that the mapper returned a string
267
+ if (typeof transformedValue !== 'string') {
268
+ throw new Error('enumMap mapper must return a string');
269
+ }
270
+ resultValues[key] = transformedValue;
271
+ }
272
+ // Create and return the new branded enum (this handles registration)
273
+ return createBrandedEnum(newId, resultValues);
274
+ }
275
+ /**
276
+ * Creates a branded enum from an array of keys where each key equals its value.
277
+ *
278
+ * This is a convenience function for the common pattern where enum keys and values
279
+ * are identical, such as `{ Active: 'Active', Inactive: 'Inactive' }`.
280
+ *
281
+ * The resulting enum is registered as an independent enum in the global registry.
282
+ * Type safety is maintained - the resulting enum's type reflects the exact literal
283
+ * types of the provided keys.
284
+ *
285
+ * @template K - The array of string keys (use `as const` for literal types)
286
+ * @param enumId - Unique identifier for the new enum. Must not already be registered.
287
+ * @param keys - Array of strings that will become both keys and values.
288
+ * Use `as const` for literal type inference.
289
+ * @returns A new branded enum where each key maps to itself
290
+ * @throws {Error} Throws `Error` with message `enumFromKeys requires at least one key`
291
+ * if keys array is empty.
292
+ * @throws {Error} Throws `Error` with message `enumFromKeys requires all keys to be non-empty strings`
293
+ * if any key is not a non-empty string.
294
+ * @throws {Error} Throws `Error` with message `enumFromKeys: duplicate key "${key}" found`
295
+ * if the keys array contains duplicates.
296
+ * @throws {Error} Throws `Error` with message `Branded enum with ID "${enumId}" already exists`
297
+ * if enumId is already registered.
298
+ *
299
+ * @example
300
+ * // Basic usage - create enum from string array
301
+ * const Status = enumFromKeys('status', ['Active', 'Inactive', 'Pending'] as const);
302
+ * // Equivalent to: { Active: 'Active', Inactive: 'Inactive', Pending: 'Pending' }
303
+ *
304
+ * Status.Active; // 'Active'
305
+ * Status.Inactive; // 'Inactive'
306
+ * Status.Pending; // 'Pending'
307
+ *
308
+ * @example
309
+ * // Type inference with as const
310
+ * const Colors = enumFromKeys('colors', ['Red', 'Green', 'Blue'] as const);
311
+ * type ColorValue = typeof Colors[keyof typeof Colors];
312
+ * // ColorValue = 'Red' | 'Green' | 'Blue'
313
+ *
314
+ * @example
315
+ * // Useful for string literal unions
316
+ * const Directions = enumFromKeys('directions', ['North', 'South', 'East', 'West'] as const);
317
+ *
318
+ * function move(direction: typeof Directions[keyof typeof Directions]) {
319
+ * // direction is 'North' | 'South' | 'East' | 'West'
320
+ * }
321
+ *
322
+ * move(Directions.North); // OK
323
+ * move('North'); // Also OK due to literal type
324
+ *
325
+ * @example
326
+ * // Error handling
327
+ * try {
328
+ * enumFromKeys('empty', []); // Empty array
329
+ * } catch (e) {
330
+ * console.log(e.message);
331
+ * // 'enumFromKeys requires at least one key'
332
+ * }
333
+ */ export function enumFromKeys(enumId, keys) {
334
+ // Validate that keys array is not empty
335
+ if (keys.length === 0) {
336
+ throw new Error('enumFromKeys requires at least one key');
337
+ }
338
+ // Track seen keys to detect duplicates
339
+ const seenKeys = new Set();
340
+ // Build the values object where each key maps to itself
341
+ const values = {};
342
+ for (const key of keys){
343
+ // Validate that each key is a non-empty string
344
+ if (typeof key !== 'string' || key.length === 0) {
345
+ throw new Error('enumFromKeys requires all keys to be non-empty strings');
346
+ }
347
+ // Check for duplicate keys
348
+ if (seenKeys.has(key)) {
349
+ throw new Error(`enumFromKeys: duplicate key "${key}" found`);
350
+ }
351
+ seenKeys.add(key);
352
+ values[key] = key;
353
+ }
354
+ // Create and return the new branded enum (this handles registration)
355
+ return createBrandedEnum(enumId, values);
356
+ }
357
+ /**
358
+ * Compares two branded enums and returns their differences.
359
+ *
360
+ * This function analyzes two branded enums and categorizes their keys into:
361
+ * - Keys only in the first enum
362
+ * - Keys only in the second enum
363
+ * - Keys in both with different values
364
+ * - Keys in both with the same values
365
+ *
366
+ * Useful for:
367
+ * - Migration: Identifying what changed between enum versions
368
+ * - Debugging: Understanding differences between similar enums
369
+ * - Validation: Ensuring enums have expected overlap or differences
370
+ *
371
+ * @template E1 - The first branded enum type
372
+ * @template E2 - The second branded enum type
373
+ * @param firstEnum - The first branded enum to compare
374
+ * @param secondEnum - The second branded enum to compare
375
+ * @returns An EnumDiffResult object containing categorized differences
376
+ * @throws {Error} Throws `Error` with message `enumDiff requires branded enums as arguments`
377
+ * if either argument is not a valid branded enum.
378
+ *
379
+ * @example
380
+ * // Compare two versions of a status enum
381
+ * const StatusV1 = createBrandedEnum('status-v1', {
382
+ * Active: 'active',
383
+ * Inactive: 'inactive',
384
+ * } as const);
385
+ *
386
+ * const StatusV2 = createBrandedEnum('status-v2', {
387
+ * Active: 'active',
388
+ * Inactive: 'disabled', // Changed value
389
+ * Pending: 'pending', // New key
390
+ * } as const);
391
+ *
392
+ * const diff = enumDiff(StatusV1, StatusV2);
393
+ * // diff.onlyInFirst: []
394
+ * // diff.onlyInSecond: [{ key: 'Pending', value: 'pending' }]
395
+ * // diff.differentValues: [{ key: 'Inactive', firstValue: 'inactive', secondValue: 'disabled' }]
396
+ * // diff.sameValues: [{ key: 'Active', value: 'active' }]
397
+ *
398
+ * @example
399
+ * // Find keys removed between versions
400
+ * const ColorsOld = createBrandedEnum('colors-old', {
401
+ * Red: 'red',
402
+ * Green: 'green',
403
+ * Blue: 'blue',
404
+ * } as const);
405
+ *
406
+ * const ColorsNew = createBrandedEnum('colors-new', {
407
+ * Red: 'red',
408
+ * Blue: 'blue',
409
+ * } as const);
410
+ *
411
+ * const diff = enumDiff(ColorsOld, ColorsNew);
412
+ * // diff.onlyInFirst: [{ key: 'Green', value: 'green' }]
413
+ * // diff.onlyInSecond: []
414
+ *
415
+ * @example
416
+ * // Check if enums are identical
417
+ * const diff = enumDiff(enumA, enumB);
418
+ * const areIdentical = diff.onlyInFirst.length === 0 &&
419
+ * diff.onlyInSecond.length === 0 &&
420
+ * diff.differentValues.length === 0;
421
+ */ export function enumDiff(firstEnum, secondEnum) {
422
+ // Validate that both arguments are branded enums
423
+ if (!isBrandedEnum(firstEnum) || !isBrandedEnum(secondEnum)) {
424
+ throw new Error('enumDiff requires branded enums as arguments');
425
+ }
426
+ // Get all keys from both enums (excluding Symbol metadata)
427
+ const firstKeys = new Set(Object.keys(firstEnum));
428
+ const secondKeys = new Set(Object.keys(secondEnum));
429
+ const onlyInFirst = [];
430
+ const onlyInSecond = [];
431
+ const differentValues = [];
432
+ const sameValues = [];
433
+ // Find keys only in first enum and keys in both
434
+ for (const key of firstKeys){
435
+ const firstValue = firstEnum[key];
436
+ if (!secondKeys.has(key)) {
437
+ // Key only exists in first enum
438
+ onlyInFirst.push({
439
+ key,
440
+ value: firstValue
441
+ });
442
+ } else {
443
+ // Key exists in both - compare values
444
+ const secondValue = secondEnum[key];
445
+ if (firstValue === secondValue) {
446
+ sameValues.push({
447
+ key,
448
+ value: firstValue
449
+ });
450
+ } else {
451
+ differentValues.push({
452
+ key,
453
+ firstValue,
454
+ secondValue
455
+ });
456
+ }
457
+ }
458
+ }
459
+ // Find keys only in second enum
460
+ for (const key of secondKeys){
461
+ if (!firstKeys.has(key)) {
462
+ const secondValue = secondEnum[key];
463
+ onlyInSecond.push({
464
+ key,
465
+ value: secondValue
466
+ });
467
+ }
468
+ }
469
+ return {
470
+ onlyInFirst,
471
+ onlyInSecond,
472
+ differentValues,
473
+ sameValues
474
+ };
475
+ }
476
+ /**
477
+ * Finds values that exist in multiple branded enums.
478
+ *
479
+ * This function analyzes multiple branded enums and identifies values that
480
+ * appear in more than one enum. For each shared value, it returns the value
481
+ * along with the IDs of all enums that contain it.
482
+ *
483
+ * Useful for:
484
+ * - Detecting value collisions across enums
485
+ * - Finding common values for potential refactoring
486
+ * - Debugging i18n key conflicts
487
+ * - Identifying intentional value overlaps
488
+ *
489
+ * @param enums - Array of branded enums to analyze for intersections
490
+ * @returns Array of EnumIntersectEntry objects, each containing a shared value
491
+ * and the IDs of enums containing that value. Only values appearing in 2+
492
+ * enums are included. Results are sorted by value for consistent ordering.
493
+ * @throws {Error} Throws `Error` with message `enumIntersect requires at least two branded enums`
494
+ * if fewer than two enums are provided.
495
+ * @throws {Error} Throws `Error` with message `enumIntersect requires all arguments to be branded enums`
496
+ * if any argument is not a valid branded enum.
497
+ *
498
+ * @example
499
+ * // Find shared values between color enums
500
+ * const PrimaryColors = createBrandedEnum('primary', {
501
+ * Red: 'red',
502
+ * Blue: 'blue',
503
+ * Yellow: 'yellow',
504
+ * } as const);
505
+ *
506
+ * const WarmColors = createBrandedEnum('warm', {
507
+ * Red: 'red',
508
+ * Orange: 'orange',
509
+ * Yellow: 'yellow',
510
+ * } as const);
511
+ *
512
+ * const shared = enumIntersect(PrimaryColors, WarmColors);
513
+ * // shared = [
514
+ * // { value: 'red', enumIds: ['primary', 'warm'] },
515
+ * // { value: 'yellow', enumIds: ['primary', 'warm'] }
516
+ * // ]
517
+ *
518
+ * @example
519
+ * // Detect i18n key collisions across multiple libraries
520
+ * const LibAKeys = createBrandedEnum('lib-a', {
521
+ * Submit: 'submit',
522
+ * Cancel: 'cancel',
523
+ * } as const);
524
+ *
525
+ * const LibBKeys = createBrandedEnum('lib-b', {
526
+ * Submit: 'submit',
527
+ * Reset: 'reset',
528
+ * } as const);
529
+ *
530
+ * const LibCKeys = createBrandedEnum('lib-c', {
531
+ * Submit: 'submit',
532
+ * Clear: 'clear',
533
+ * } as const);
534
+ *
535
+ * const collisions = enumIntersect(LibAKeys, LibBKeys, LibCKeys);
536
+ * // collisions = [
537
+ * // { value: 'submit', enumIds: ['lib-a', 'lib-b', 'lib-c'] }
538
+ * // ]
539
+ *
540
+ * @example
541
+ * // Check if enums have any overlap
542
+ * const shared = enumIntersect(enumA, enumB);
543
+ * if (shared.length === 0) {
544
+ * console.log('No shared values between enums');
545
+ * }
546
+ */ export function enumIntersect(...enums) {
547
+ // Validate that at least two enums are provided
548
+ if (enums.length < 2) {
549
+ throw new Error('enumIntersect requires at least two branded enums');
550
+ }
551
+ // Validate that all arguments are branded enums
552
+ for (const enumObj of enums){
553
+ if (!isBrandedEnum(enumObj)) {
554
+ throw new Error('enumIntersect requires all arguments to be branded enums');
555
+ }
556
+ }
557
+ // Build a map of value -> Set of enum IDs containing that value
558
+ const valueToEnumIds = new Map();
559
+ for (const enumObj of enums){
560
+ const enumId = enumObj[ENUM_ID];
561
+ const values = enumObj[ENUM_VALUES];
562
+ for (const value of values){
563
+ if (!valueToEnumIds.has(value)) {
564
+ valueToEnumIds.set(value, new Set());
565
+ }
566
+ valueToEnumIds.get(value).add(enumId);
567
+ }
568
+ }
569
+ // Filter to only values that appear in multiple enums
570
+ const result = [];
571
+ for (const [value, enumIds] of valueToEnumIds){
572
+ if (enumIds.size >= 2) {
573
+ result.push({
574
+ value,
575
+ enumIds: Array.from(enumIds).sort()
576
+ });
577
+ }
578
+ }
579
+ // Sort by value for consistent ordering
580
+ result.sort((a, b)=>a.value.localeCompare(b.value));
581
+ return result;
582
+ }
583
+ /**
584
+ * Converts a branded enum to a plain Record object, stripping all metadata.
585
+ *
586
+ * This function creates a new plain object containing only the key-value pairs
587
+ * from the branded enum, without any Symbol metadata properties. The result is
588
+ * a simple `Record<string, string>` that can be safely serialized, spread, or
589
+ * used in contexts where branded enum metadata is not needed.
590
+ *
591
+ * Useful for:
592
+ * - Serialization scenarios where you need a plain object
593
+ * - Interoperability with APIs that expect plain objects
594
+ * - Creating snapshots of enum state without metadata
595
+ * - Passing enum data to external systems
596
+ *
597
+ * @template E - The branded enum type
598
+ * @param enumObj - The branded enum to convert
599
+ * @returns A plain Record<string, string> containing only the key-value pairs
600
+ * @throws {Error} Throws `Error` with message `enumToRecord requires a branded enum`
601
+ * if enumObj is not a valid branded enum.
602
+ *
603
+ * @example
604
+ * // Basic usage - convert to plain object
605
+ * const Status = createBrandedEnum('status', {
606
+ * Active: 'active',
607
+ * Inactive: 'inactive',
608
+ * Pending: 'pending',
609
+ * } as const);
610
+ *
611
+ * const plainStatus = enumToRecord(Status);
612
+ * // plainStatus = { Active: 'active', Inactive: 'inactive', Pending: 'pending' }
613
+ * // No Symbol metadata, just a plain object
614
+ *
615
+ * @example
616
+ * // Serialization scenario
617
+ * const Colors = createBrandedEnum('colors', {
618
+ * Red: 'red',
619
+ * Green: 'green',
620
+ * Blue: 'blue',
621
+ * } as const);
622
+ *
623
+ * // Send to an API that expects plain objects
624
+ * const payload = {
625
+ * availableColors: enumToRecord(Colors),
626
+ * };
627
+ * await fetch('/api/config', {
628
+ * method: 'POST',
629
+ * body: JSON.stringify(payload),
630
+ * });
631
+ *
632
+ * @example
633
+ * // Comparing with spread operator
634
+ * const Status = createBrandedEnum('status', { Active: 'active' } as const);
635
+ *
636
+ * // Both produce the same result for enumerable properties:
637
+ * const spread = { ...Status };
638
+ * const record = enumToRecord(Status);
639
+ * // spread and record are equivalent plain objects
640
+ *
641
+ * // But enumToRecord is explicit about intent and validates input
642
+ *
643
+ * @example
644
+ * // Type safety
645
+ * const Sizes = createBrandedEnum('sizes', {
646
+ * Small: 's',
647
+ * Medium: 'm',
648
+ * Large: 'l',
649
+ * } as const);
650
+ *
651
+ * const record = enumToRecord(Sizes);
652
+ * // record type is Record<string, string>
653
+ * // Can be used anywhere a plain object is expected
654
+ */ export function enumToRecord(enumObj) {
655
+ // Validate that enumObj is a branded enum
656
+ if (!isBrandedEnum(enumObj)) {
657
+ throw new Error('enumToRecord requires a branded enum');
658
+ }
659
+ // Create a new plain object with only the enumerable key-value pairs
660
+ const result = {};
661
+ for (const key of Object.keys(enumObj)){
662
+ result[key] = enumObj[key];
663
+ }
664
+ return result;
665
+ }
666
+ /**
667
+ * The key used to store the enum watcher registry on globalThis.
668
+ * Namespaced to avoid collisions with other libraries.
669
+ */ const WATCHER_REGISTRY_KEY = '__brandedEnumWatcherRegistry__';
670
+ /**
671
+ * Gets or initializes the watcher registry on globalThis.
672
+ */ function getWatcherRegistry() {
673
+ const global = globalThis;
674
+ if (!global[WATCHER_REGISTRY_KEY]) {
675
+ global[WATCHER_REGISTRY_KEY] = {
676
+ watchers: new Map(),
677
+ globalWatchers: new Set()
678
+ };
679
+ }
680
+ return global[WATCHER_REGISTRY_KEY];
681
+ }
682
+ /**
683
+ * Creates a watched version of a branded enum that triggers callbacks on access.
684
+ *
685
+ * This function wraps a branded enum in a Proxy that intercepts property access
686
+ * and calls registered callbacks. This is useful for debugging, development tooling,
687
+ * and understanding how enums are used throughout an application.
688
+ *
689
+ * The watched enum behaves identically to the original enum - all values, metadata,
690
+ * and type information are preserved. The only difference is that access events
691
+ * are reported to the callback.
692
+ *
693
+ * **Note:** This feature is intended for development and debugging purposes.
694
+ * Using watched enums in production may have performance implications due to
695
+ * the Proxy overhead.
696
+ *
697
+ * @template E - The branded enum type
698
+ * @param enumObj - The branded enum to watch
699
+ * @param callback - Function called whenever the enum is accessed
700
+ * @returns An object containing the watched enum proxy and an unwatch function
701
+ * @throws {Error} Throws `Error` with message `watchEnum requires a branded enum`
702
+ * if enumObj is not a valid branded enum.
703
+ *
704
+ * @example
705
+ * // Basic usage - log all enum accesses
706
+ * const Status = createBrandedEnum('status', {
707
+ * Active: 'active',
708
+ * Inactive: 'inactive',
709
+ * } as const);
710
+ *
711
+ * const { watched, unwatch } = watchEnum(Status, (event) => {
712
+ * console.log(`Accessed ${event.enumId}.${event.key} = ${event.value}`);
713
+ * });
714
+ *
715
+ * watched.Active; // Logs: "Accessed status.Active = active"
716
+ * watched.Inactive; // Logs: "Accessed status.Inactive = inactive"
717
+ *
718
+ * // Stop watching
719
+ * unwatch();
720
+ * watched.Active; // No longer logs
721
+ *
722
+ * @example
723
+ * // Track enum usage for debugging
724
+ * const accessLog: EnumAccessEvent[] = [];
725
+ *
726
+ * const { watched: WatchedColors } = watchEnum(Colors, (event) => {
727
+ * accessLog.push(event);
728
+ * });
729
+ *
730
+ * // Use the watched enum in your code
731
+ * doSomething(WatchedColors.Red);
732
+ * doSomethingElse(WatchedColors.Blue);
733
+ *
734
+ * // Later, analyze the access log
735
+ * console.log(`Enum accessed ${accessLog.length} times`);
736
+ * console.log('Keys accessed:', accessLog.map(e => e.key));
737
+ *
738
+ * @example
739
+ * // Detect unused enum values
740
+ * const usedKeys = new Set<string>();
741
+ *
742
+ * const { watched } = watchEnum(MyEnum, (event) => {
743
+ * if (event.key) usedKeys.add(event.key);
744
+ * });
745
+ *
746
+ * // After running your application/tests
747
+ * const allKeys = Object.keys(MyEnum);
748
+ * const unusedKeys = allKeys.filter(k => !usedKeys.has(k));
749
+ * console.log('Unused enum keys:', unusedKeys);
750
+ *
751
+ * @example
752
+ * // Performance monitoring
753
+ * const { watched } = watchEnum(Status, (event) => {
754
+ * if (event.accessType === 'get') {
755
+ * performance.mark(`enum-access-${event.enumId}-${event.key}`);
756
+ * }
757
+ * });
758
+ */ export function watchEnum(enumObj, callback) {
759
+ // Validate that enumObj is a branded enum
760
+ if (!isBrandedEnum(enumObj)) {
761
+ throw new Error('watchEnum requires a branded enum');
762
+ }
763
+ const enumId = enumObj[ENUM_ID];
764
+ const registry = getWatcherRegistry();
765
+ // Add callback to the registry for this enum
766
+ if (!registry.watchers.has(enumId)) {
767
+ registry.watchers.set(enumId, new Set());
768
+ }
769
+ registry.watchers.get(enumId).add(callback);
770
+ // Track if this watcher is still active
771
+ let isActive = true;
772
+ // Create a Proxy to intercept access
773
+ const proxy = new Proxy(enumObj, {
774
+ get (target, prop, receiver) {
775
+ const value = Reflect.get(target, prop, receiver);
776
+ // Only trigger callback for active watchers and string keys (not symbols)
777
+ if (isActive && typeof prop === 'string') {
778
+ const event = {
779
+ enumId,
780
+ accessType: 'get',
781
+ key: prop,
782
+ value: typeof value === 'string' ? value : undefined,
783
+ timestamp: Date.now()
784
+ };
785
+ // Call the specific callback
786
+ callback(event);
787
+ // Also call any global watchers
788
+ for (const globalCallback of registry.globalWatchers){
789
+ globalCallback(event);
790
+ }
791
+ }
792
+ return value;
793
+ },
794
+ has (target, prop) {
795
+ const result = Reflect.has(target, prop);
796
+ // Only trigger callback for active watchers and string keys
797
+ if (isActive && typeof prop === 'string') {
798
+ const event = {
799
+ enumId,
800
+ accessType: 'has',
801
+ key: prop,
802
+ timestamp: Date.now()
803
+ };
804
+ callback(event);
805
+ for (const globalCallback of registry.globalWatchers){
806
+ globalCallback(event);
807
+ }
808
+ }
809
+ return result;
810
+ },
811
+ ownKeys (target) {
812
+ const keys = Reflect.ownKeys(target);
813
+ if (isActive) {
814
+ const event = {
815
+ enumId,
816
+ accessType: 'keys',
817
+ timestamp: Date.now()
818
+ };
819
+ callback(event);
820
+ for (const globalCallback of registry.globalWatchers){
821
+ globalCallback(event);
822
+ }
823
+ }
824
+ return keys;
825
+ },
826
+ getOwnPropertyDescriptor (target, prop) {
827
+ return Reflect.getOwnPropertyDescriptor(target, prop);
828
+ }
829
+ });
830
+ // Create unwatch function
831
+ const unwatch = ()=>{
832
+ isActive = false;
833
+ const callbacks = registry.watchers.get(enumId);
834
+ if (callbacks) {
835
+ callbacks.delete(callback);
836
+ if (callbacks.size === 0) {
837
+ registry.watchers.delete(enumId);
838
+ }
839
+ }
840
+ };
841
+ return {
842
+ watched: proxy,
843
+ unwatch
844
+ };
845
+ }
846
+ /**
847
+ * Registers a global callback that receives access events from all watched enums.
848
+ *
849
+ * This is useful for centralized logging or monitoring of enum usage across
850
+ * an entire application without needing to wrap each enum individually.
851
+ *
852
+ * @param callback - Function called for every enum access event
853
+ * @returns A function to unregister the global callback
854
+ *
855
+ * @example
856
+ * // Centralized enum access logging
857
+ * const unregister = watchAllEnums((event) => {
858
+ * console.log(`[${event.enumId}] ${event.accessType}: ${event.key}`);
859
+ * });
860
+ *
861
+ * // All watched enums will now trigger this callback
862
+ * watchedStatus.Active; // Logs: "[status] get: Active"
863
+ * watchedColors.Red; // Logs: "[colors] get: Red"
864
+ *
865
+ * // Stop global watching
866
+ * unregister();
867
+ */ export function watchAllEnums(callback) {
868
+ const registry = getWatcherRegistry();
869
+ registry.globalWatchers.add(callback);
870
+ return ()=>{
871
+ registry.globalWatchers.delete(callback);
872
+ };
873
+ }
874
+ /**
875
+ * Clears all enum watchers (both specific and global).
876
+ *
877
+ * This is primarily useful for testing or when you need to reset
878
+ * the watcher state completely.
879
+ *
880
+ * @example
881
+ * // In test cleanup
882
+ * afterEach(() => {
883
+ * clearAllEnumWatchers();
884
+ * });
885
+ */ export function clearAllEnumWatchers() {
886
+ const registry = getWatcherRegistry();
887
+ registry.watchers.clear();
888
+ registry.globalWatchers.clear();
889
+ }
890
+ /**
891
+ * Gets the number of active watchers for a specific enum.
892
+ *
893
+ * @param enumId - The enum ID to check
894
+ * @returns The number of active watchers for this enum
895
+ *
896
+ * @example
897
+ * const { watched } = watchEnum(Status, callback1);
898
+ * watchEnum(Status, callback2);
899
+ *
900
+ * getEnumWatcherCount('status'); // 2
901
+ */ export function getEnumWatcherCount(enumId) {
902
+ const registry = getWatcherRegistry();
903
+ return registry.watchers.get(enumId)?.size ?? 0;
904
+ }
905
+ /**
906
+ * Gets the number of global watchers.
907
+ *
908
+ * @returns The number of active global watchers
909
+ */ export function getGlobalWatcherCount() {
910
+ const registry = getWatcherRegistry();
911
+ return registry.globalWatchers.size;
912
+ }
913
+ /**
914
+ * A helper function for exhaustiveness checking in switch statements.
915
+ *
916
+ * This function should be called in the `default` case of a switch statement
917
+ * when you want to ensure all cases are handled. If the switch is exhaustive,
918
+ * TypeScript will infer that this function is never called (the value is `never`).
919
+ * If a case is missing, TypeScript will show a compile-time error.
920
+ *
921
+ * At runtime, if this function is somehow called (e.g., due to a type assertion
922
+ * or JavaScript interop), it throws a descriptive error.
923
+ *
924
+ * @param value - The value that should be unreachable. TypeScript should infer this as `never`.
925
+ * @param message - Optional custom error message. Defaults to a descriptive message including the value.
926
+ * @returns Never returns - always throws an error if called at runtime.
927
+ * @throws {Error} Throws `Error` with message `Exhaustive check failed: unexpected value "${value}"`
928
+ * or the custom message if provided.
929
+ *
930
+ * @example
931
+ * // Basic usage with a branded enum
932
+ * const Status = createBrandedEnum('status', {
933
+ * Active: 'active',
934
+ * Inactive: 'inactive',
935
+ * Pending: 'pending',
936
+ * } as const);
937
+ *
938
+ * type StatusValue = typeof Status[keyof typeof Status];
939
+ *
940
+ * function handleStatus(status: StatusValue): string {
941
+ * switch (status) {
942
+ * case Status.Active:
943
+ * return 'User is active';
944
+ * case Status.Inactive:
945
+ * return 'User is inactive';
946
+ * case Status.Pending:
947
+ * return 'User is pending';
948
+ * default:
949
+ * // TypeScript knows this is unreachable if all cases are handled
950
+ * return exhaustive(status);
951
+ * }
952
+ * }
953
+ *
954
+ * @example
955
+ * // Compile-time error when a case is missing
956
+ * function handleStatusIncomplete(status: StatusValue): string {
957
+ * switch (status) {
958
+ * case Status.Active:
959
+ * return 'User is active';
960
+ * case Status.Inactive:
961
+ * return 'User is inactive';
962
+ * // Missing: case Status.Pending
963
+ * default:
964
+ * // TypeScript error: Argument of type '"pending"' is not assignable to parameter of type 'never'
965
+ * return exhaustive(status);
966
+ * }
967
+ * }
968
+ *
969
+ * @example
970
+ * // Custom error message
971
+ * function processValue(value: StatusValue): void {
972
+ * switch (value) {
973
+ * case Status.Active:
974
+ * case Status.Inactive:
975
+ * case Status.Pending:
976
+ * console.log('Handled:', value);
977
+ * break;
978
+ * default:
979
+ * exhaustive(value, `Unknown status value encountered: ${value}`);
980
+ * }
981
+ * }
982
+ *
983
+ * @example
984
+ * // Works with any discriminated union, not just branded enums
985
+ * type Shape =
986
+ * | { kind: 'circle'; radius: number }
987
+ * | { kind: 'square'; side: number }
988
+ * | { kind: 'rectangle'; width: number; height: number };
989
+ *
990
+ * function getArea(shape: Shape): number {
991
+ * switch (shape.kind) {
992
+ * case 'circle':
993
+ * return Math.PI * shape.radius ** 2;
994
+ * case 'square':
995
+ * return shape.side ** 2;
996
+ * case 'rectangle':
997
+ * return shape.width * shape.height;
998
+ * default:
999
+ * return exhaustive(shape);
1000
+ * }
1001
+ * }
1002
+ */ export function exhaustive(value, message) {
1003
+ const errorMessage = message ?? `Exhaustive check failed: unexpected value "${String(value)}"`;
1004
+ throw new Error(errorMessage);
1005
+ }
1006
+ /**
1007
+ * Creates an exhaustiveness guard function bound to a specific branded enum.
1008
+ *
1009
+ * This factory function returns a guard function that can be used in switch
1010
+ * statement default cases. The returned function provides better error messages
1011
+ * by including the enum ID in the error.
1012
+ *
1013
+ * This is useful when you want to:
1014
+ * - Have consistent error messages that include the enum name
1015
+ * - Create reusable guards for specific enums
1016
+ * - Provide more context in error messages for debugging
1017
+ *
1018
+ * @template E - The branded enum type
1019
+ * @param enumObj - The branded enum to create a guard for
1020
+ * @returns A function that throws an error with the enum ID included in the message
1021
+ * @throws {Error} Throws `Error` with message `exhaustiveGuard requires a branded enum`
1022
+ * if enumObj is not a valid branded enum.
1023
+ *
1024
+ * @example
1025
+ * // Create a guard for a specific enum
1026
+ * const Status = createBrandedEnum('status', {
1027
+ * Active: 'active',
1028
+ * Inactive: 'inactive',
1029
+ * Pending: 'pending',
1030
+ * } as const);
1031
+ *
1032
+ * const assertStatusExhaustive = exhaustiveGuard(Status);
1033
+ *
1034
+ * type StatusValue = typeof Status[keyof typeof Status];
1035
+ *
1036
+ * function handleStatus(status: StatusValue): string {
1037
+ * switch (status) {
1038
+ * case Status.Active:
1039
+ * return 'Active';
1040
+ * case Status.Inactive:
1041
+ * return 'Inactive';
1042
+ * case Status.Pending:
1043
+ * return 'Pending';
1044
+ * default:
1045
+ * // Error message will include "status" enum ID
1046
+ * return assertStatusExhaustive(status);
1047
+ * }
1048
+ * }
1049
+ *
1050
+ * @example
1051
+ * // Inline usage without storing the guard
1052
+ * function processStatus(status: StatusValue): void {
1053
+ * switch (status) {
1054
+ * case Status.Active:
1055
+ * case Status.Inactive:
1056
+ * case Status.Pending:
1057
+ * console.log('Processing:', status);
1058
+ * break;
1059
+ * default:
1060
+ * exhaustiveGuard(Status)(status);
1061
+ * }
1062
+ * }
1063
+ *
1064
+ * @example
1065
+ * // Runtime error message example
1066
+ * // If somehow called with an unexpected value at runtime:
1067
+ * // Error: Exhaustive check failed for enum "status": unexpected value "unknown"
1068
+ *
1069
+ * @example
1070
+ * // Compile-time error when case is missing
1071
+ * function incompleteHandler(status: StatusValue): string {
1072
+ * switch (status) {
1073
+ * case Status.Active:
1074
+ * return 'Active';
1075
+ * // Missing: Inactive and Pending cases
1076
+ * default:
1077
+ * // TypeScript error: Argument of type '"inactive" | "pending"' is not assignable to parameter of type 'never'
1078
+ * return assertStatusExhaustive(status);
1079
+ * }
1080
+ * }
1081
+ */ export function exhaustiveGuard(enumObj) {
1082
+ // Validate that enumObj is a branded enum
1083
+ if (!isBrandedEnum(enumObj)) {
1084
+ throw new Error('exhaustiveGuard requires a branded enum');
1085
+ }
1086
+ const enumId = enumObj[ENUM_ID];
1087
+ return (value)=>{
1088
+ throw new Error(`Exhaustive check failed for enum "${enumId}": unexpected value "${String(value)}"`);
1089
+ };
1090
+ }
1091
+ /**
1092
+ * Maps schema version options to their corresponding $schema URIs.
1093
+ */ const SCHEMA_VERSION_URIS = {
1094
+ 'draft-04': 'http://json-schema.org/draft-04/schema#',
1095
+ 'draft-06': 'http://json-schema.org/draft-06/schema#',
1096
+ 'draft-07': 'http://json-schema.org/draft-07/schema#',
1097
+ '2019-09': 'https://json-schema.org/draft/2019-09/schema',
1098
+ '2020-12': 'https://json-schema.org/draft/2020-12/schema'
1099
+ };
1100
+ /**
1101
+ * Generates a JSON Schema from a branded enum.
1102
+ *
1103
+ * This function creates a JSON Schema object that validates strings against
1104
+ * the values of a branded enum. The generated schema can be used with any
1105
+ * JSON Schema validator to ensure that input values are valid enum members.
1106
+ *
1107
+ * The schema includes:
1108
+ * - `$schema`: The JSON Schema version URI (optional, defaults to draft-07)
1109
+ * - `title`: The enum ID or a custom title
1110
+ * - `description`: A description of the enum
1111
+ * - `type`: Always 'string' for branded enums
1112
+ * - `enum`: An array of all valid enum values
1113
+ *
1114
+ * @template E - The branded enum type
1115
+ * @param enumObj - The branded enum to generate a schema for
1116
+ * @param options - Optional configuration for schema generation
1117
+ * @returns A JSON Schema object that validates against the enum values
1118
+ * @throws {Error} Throws `Error` with message `toJsonSchema requires a branded enum`
1119
+ * if enumObj is not a valid branded enum.
1120
+ *
1121
+ * @example
1122
+ * // Basic usage - generate schema with defaults
1123
+ * const Status = createBrandedEnum('status', {
1124
+ * Active: 'active',
1125
+ * Inactive: 'inactive',
1126
+ * Pending: 'pending',
1127
+ * } as const);
1128
+ *
1129
+ * const schema = toJsonSchema(Status);
1130
+ * // {
1131
+ * // $schema: 'http://json-schema.org/draft-07/schema#',
1132
+ * // title: 'status',
1133
+ * // description: 'Enum values for status',
1134
+ * // type: 'string',
1135
+ * // enum: ['active', 'inactive', 'pending']
1136
+ * // }
1137
+ *
1138
+ * @example
1139
+ * // Custom title and description
1140
+ * const Priority = createBrandedEnum('priority', {
1141
+ * High: 'high',
1142
+ * Medium: 'medium',
1143
+ * Low: 'low',
1144
+ * } as const);
1145
+ *
1146
+ * const schema = toJsonSchema(Priority, {
1147
+ * title: 'Task Priority',
1148
+ * description: 'The priority level of a task',
1149
+ * });
1150
+ * // {
1151
+ * // $schema: 'http://json-schema.org/draft-07/schema#',
1152
+ * // title: 'Task Priority',
1153
+ * // description: 'The priority level of a task',
1154
+ * // type: 'string',
1155
+ * // enum: ['high', 'medium', 'low']
1156
+ * // }
1157
+ *
1158
+ * @example
1159
+ * // Without $schema property
1160
+ * const schema = toJsonSchema(Status, { includeSchema: false });
1161
+ * // {
1162
+ * // title: 'status',
1163
+ * // description: 'Enum values for status',
1164
+ * // type: 'string',
1165
+ * // enum: ['active', 'inactive', 'pending']
1166
+ * // }
1167
+ *
1168
+ * @example
1169
+ * // Using a different schema version
1170
+ * const schema = toJsonSchema(Status, { schemaVersion: '2020-12' });
1171
+ * // {
1172
+ * // $schema: 'https://json-schema.org/draft/2020-12/schema',
1173
+ * // title: 'status',
1174
+ * // description: 'Enum values for status',
1175
+ * // type: 'string',
1176
+ * // enum: ['active', 'inactive', 'pending']
1177
+ * // }
1178
+ *
1179
+ * @example
1180
+ * // Use with JSON Schema validators
1181
+ * import Ajv from 'ajv';
1182
+ *
1183
+ * const schema = toJsonSchema(Status);
1184
+ * const ajv = new Ajv();
1185
+ * const validate = ajv.compile(schema);
1186
+ *
1187
+ * validate('active'); // true
1188
+ * validate('inactive'); // true
1189
+ * validate('unknown'); // false
1190
+ *
1191
+ * @example
1192
+ * // Embed in a larger schema
1193
+ * const userSchema = {
1194
+ * type: 'object',
1195
+ * properties: {
1196
+ * name: { type: 'string' },
1197
+ * status: toJsonSchema(Status, { includeSchema: false }),
1198
+ * },
1199
+ * required: ['name', 'status'],
1200
+ * };
1201
+ *
1202
+ * @example
1203
+ * // Generate schemas for API documentation
1204
+ * const schemas = {
1205
+ * Status: toJsonSchema(Status),
1206
+ * Priority: toJsonSchema(Priority),
1207
+ * };
1208
+ *
1209
+ * // Export for OpenAPI/Swagger
1210
+ * const openApiComponents = {
1211
+ * schemas: Object.fromEntries(
1212
+ * Object.entries(schemas).map(([name, schema]) => [
1213
+ * name,
1214
+ * { ...schema, $schema: undefined }, // OpenAPI doesn't use $schema
1215
+ * ])
1216
+ * ),
1217
+ * };
1218
+ */ export function toJsonSchema(enumObj, options = {}) {
1219
+ // Validate that enumObj is a branded enum
1220
+ if (!isBrandedEnum(enumObj)) {
1221
+ throw new Error('toJsonSchema requires a branded enum');
1222
+ }
1223
+ const enumId = enumObj[ENUM_ID];
1224
+ const values = enumObj[ENUM_VALUES];
1225
+ // Extract options with defaults
1226
+ const { title = enumId, description = `Enum values for ${enumId}`, includeSchema = true, schemaVersion = 'draft-07' } = options;
1227
+ // Build the schema object
1228
+ const schema = {
1229
+ title,
1230
+ description,
1231
+ type: 'string',
1232
+ enum: Array.from(values).sort()
1233
+ };
1234
+ // Add $schema if requested
1235
+ if (includeSchema) {
1236
+ return {
1237
+ $schema: SCHEMA_VERSION_URIS[schemaVersion],
1238
+ ...schema
1239
+ };
1240
+ }
1241
+ return schema;
1242
+ }
1243
+ /**
1244
+ * Generates a Zod-compatible schema definition from a branded enum.
1245
+ *
1246
+ * This function creates a schema definition object that can be used to
1247
+ * construct a Zod enum schema. The library maintains zero dependencies
1248
+ * by returning a definition object rather than a Zod instance.
1249
+ *
1250
+ * The returned definition includes:
1251
+ * - `typeName`: Always 'ZodEnum' to identify the schema type
1252
+ * - `values`: A tuple of all enum values (sorted for consistency)
1253
+ * - `description`: Optional description for the schema
1254
+ * - `enumId`: The branded enum's ID for reference
1255
+ *
1256
+ * **Zero Dependencies**: This function does not import or depend on Zod.
1257
+ * It returns a plain object that you can use to construct a Zod schema
1258
+ * in your own code where Zod is available.
1259
+ *
1260
+ * @template E - The branded enum type
1261
+ * @param enumObj - The branded enum to generate a schema definition for
1262
+ * @param options - Optional configuration for schema generation
1263
+ * @returns A Zod-compatible schema definition object
1264
+ * @throws {Error} Throws `Error` with message `toZodSchema requires a branded enum`
1265
+ * if enumObj is not a valid branded enum.
1266
+ * @throws {Error} Throws `Error` with message `toZodSchema requires an enum with at least one value`
1267
+ * if the enum has no values (Zod requires at least one value for z.enum()).
1268
+ *
1269
+ * @example
1270
+ * // Basic usage - generate schema definition
1271
+ * const Status = createBrandedEnum('status', {
1272
+ * Active: 'active',
1273
+ * Inactive: 'inactive',
1274
+ * Pending: 'pending',
1275
+ * } as const);
1276
+ *
1277
+ * const schemaDef = toZodSchema(Status);
1278
+ * // {
1279
+ * // typeName: 'ZodEnum',
1280
+ * // values: ['active', 'inactive', 'pending'],
1281
+ * // enumId: 'status'
1282
+ * // }
1283
+ *
1284
+ * @example
1285
+ * // Use with Zod to create an actual schema
1286
+ * import { z } from 'zod';
1287
+ *
1288
+ * const Status = createBrandedEnum('status', {
1289
+ * Active: 'active',
1290
+ * Inactive: 'inactive',
1291
+ * } as const);
1292
+ *
1293
+ * const def = toZodSchema(Status);
1294
+ * const statusSchema = z.enum(def.values);
1295
+ *
1296
+ * // Validate values
1297
+ * statusSchema.parse('active'); // 'active'
1298
+ * statusSchema.parse('invalid'); // throws ZodError
1299
+ *
1300
+ * @example
1301
+ * // With description
1302
+ * const Priority = createBrandedEnum('priority', {
1303
+ * High: 'high',
1304
+ * Medium: 'medium',
1305
+ * Low: 'low',
1306
+ * } as const);
1307
+ *
1308
+ * const def = toZodSchema(Priority, {
1309
+ * description: 'Task priority level',
1310
+ * });
1311
+ * // {
1312
+ * // typeName: 'ZodEnum',
1313
+ * // values: ['high', 'low', 'medium'],
1314
+ * // description: 'Task priority level',
1315
+ * // enumId: 'priority'
1316
+ * // }
1317
+ *
1318
+ * // Use with Zod
1319
+ * const schema = z.enum(def.values).describe(def.description!);
1320
+ *
1321
+ * @example
1322
+ * // Type-safe schema creation helper
1323
+ * import { z } from 'zod';
1324
+ *
1325
+ * function createZodEnumFromBranded<E extends BrandedEnum<Record<string, string>>>(
1326
+ * enumObj: E,
1327
+ * description?: string
1328
+ * ) {
1329
+ * const def = toZodSchema(enumObj, { description });
1330
+ * const schema = z.enum(def.values);
1331
+ * return description ? schema.describe(description) : schema;
1332
+ * }
1333
+ *
1334
+ * const statusSchema = createZodEnumFromBranded(Status, 'User status');
1335
+ *
1336
+ * @example
1337
+ * // Generate schemas for multiple enums
1338
+ * const schemas = {
1339
+ * status: toZodSchema(Status),
1340
+ * priority: toZodSchema(Priority),
1341
+ * category: toZodSchema(Category),
1342
+ * };
1343
+ *
1344
+ * // Later, construct Zod schemas as needed
1345
+ * import { z } from 'zod';
1346
+ * const zodSchemas = Object.fromEntries(
1347
+ * Object.entries(schemas).map(([key, def]) => [key, z.enum(def.values)])
1348
+ * );
1349
+ *
1350
+ * @example
1351
+ * // Use in form validation
1352
+ * import { z } from 'zod';
1353
+ *
1354
+ * const statusDef = toZodSchema(Status);
1355
+ * const priorityDef = toZodSchema(Priority);
1356
+ *
1357
+ * const taskSchema = z.object({
1358
+ * title: z.string().min(1),
1359
+ * status: z.enum(statusDef.values),
1360
+ * priority: z.enum(priorityDef.values),
1361
+ * });
1362
+ *
1363
+ * type Task = z.infer<typeof taskSchema>;
1364
+ * // { title: string; status: 'active' | 'inactive' | 'pending'; priority: 'high' | 'medium' | 'low' }
1365
+ */ export function toZodSchema(enumObj, options = {}) {
1366
+ // Validate that enumObj is a branded enum
1367
+ if (!isBrandedEnum(enumObj)) {
1368
+ throw new Error('toZodSchema requires a branded enum');
1369
+ }
1370
+ const enumId = enumObj[ENUM_ID];
1371
+ const values = enumObj[ENUM_VALUES];
1372
+ // Convert Set to sorted array
1373
+ const valuesArray = Array.from(values).sort();
1374
+ // Zod's z.enum() requires at least one value
1375
+ if (valuesArray.length === 0) {
1376
+ throw new Error('toZodSchema requires an enum with at least one value');
1377
+ }
1378
+ // Build the schema definition
1379
+ const definition = {
1380
+ typeName: 'ZodEnum',
1381
+ values: valuesArray,
1382
+ enumId
1383
+ };
1384
+ // Add description if provided
1385
+ if (options.description !== undefined) {
1386
+ return {
1387
+ ...definition,
1388
+ description: options.description
1389
+ };
1390
+ }
1391
+ return definition;
1392
+ }
1393
+ /**
1394
+ * Creates a serializer/deserializer pair for a branded enum.
1395
+ *
1396
+ * The serializer provides methods to convert enum values to strings (with optional
1397
+ * transformation) and to deserialize strings back to validated enum values.
1398
+ *
1399
+ * This is useful for:
1400
+ * - Storing enum values in databases or localStorage with custom formats
1401
+ * - Transmitting enum values over APIs with encoding/decoding
1402
+ * - Migrating between different value formats
1403
+ * - Adding prefixes/suffixes for namespacing during serialization
1404
+ *
1405
+ * **Serialization Flow:**
1406
+ * 1. Take an enum value
1407
+ * 2. Apply custom `serialize` transform (if provided)
1408
+ * 3. Return the serialized string
1409
+ *
1410
+ * **Deserialization Flow:**
1411
+ * 1. Take a string input
1412
+ * 2. Apply custom `deserialize` transform (if provided)
1413
+ * 3. Validate the result against the enum
1414
+ * 4. Return success with the value, or failure with error details
1415
+ *
1416
+ * @template E - The branded enum type
1417
+ * @param enumObj - The branded enum to create a serializer for
1418
+ * @param options - Optional configuration for custom transforms
1419
+ * @returns An EnumSerializer object with serialize and deserialize methods
1420
+ * @throws {Error} Throws `Error` with message `enumSerializer requires a branded enum`
1421
+ * if enumObj is not a valid branded enum.
1422
+ *
1423
+ * @example
1424
+ * // Basic usage without transforms
1425
+ * const Status = createBrandedEnum('status', {
1426
+ * Active: 'active',
1427
+ * Inactive: 'inactive',
1428
+ * } as const);
1429
+ *
1430
+ * const serializer = enumSerializer(Status);
1431
+ *
1432
+ * // Serialize
1433
+ * const serialized = serializer.serialize(Status.Active); // 'active'
1434
+ *
1435
+ * // Deserialize
1436
+ * const result = serializer.deserialize('active');
1437
+ * if (result.success) {
1438
+ * console.log(result.value); // 'active'
1439
+ * }
1440
+ *
1441
+ * @example
1442
+ * // With custom transforms - add prefix during serialization
1443
+ * const Priority = createBrandedEnum('priority', {
1444
+ * High: 'high',
1445
+ * Medium: 'medium',
1446
+ * Low: 'low',
1447
+ * } as const);
1448
+ *
1449
+ * const serializer = enumSerializer(Priority, {
1450
+ * serialize: (value) => `priority:${value}`,
1451
+ * deserialize: (value) => value.replace('priority:', ''),
1452
+ * });
1453
+ *
1454
+ * // Serialize adds prefix
1455
+ * serializer.serialize(Priority.High); // 'priority:high'
1456
+ *
1457
+ * // Deserialize removes prefix and validates
1458
+ * const result = serializer.deserialize('priority:high');
1459
+ * if (result.success) {
1460
+ * console.log(result.value); // 'high'
1461
+ * }
1462
+ *
1463
+ * @example
1464
+ * // Base64 encoding for storage
1465
+ * const Secret = createBrandedEnum('secret', {
1466
+ * Token: 'token',
1467
+ * Key: 'key',
1468
+ * } as const);
1469
+ *
1470
+ * const serializer = enumSerializer(Secret, {
1471
+ * serialize: (value) => btoa(value),
1472
+ * deserialize: (value) => atob(value),
1473
+ * });
1474
+ *
1475
+ * serializer.serialize(Secret.Token); // 'dG9rZW4=' (base64 of 'token')
1476
+ * serializer.deserialize('dG9rZW4='); // { success: true, value: 'token' }
1477
+ *
1478
+ * @example
1479
+ * // Error handling
1480
+ * const result = serializer.deserialize('invalid');
1481
+ * if (!result.success) {
1482
+ * console.log(result.error.message);
1483
+ * // 'Value "invalid" is not a member of enum "status"'
1484
+ * console.log(result.error.validValues);
1485
+ * // ['active', 'inactive']
1486
+ * }
1487
+ *
1488
+ * @example
1489
+ * // Using deserializeOrThrow for simpler code when errors should throw
1490
+ * try {
1491
+ * const value = serializer.deserializeOrThrow('active');
1492
+ * console.log(value); // 'active'
1493
+ * } catch (e) {
1494
+ * console.error('Invalid value:', e.message);
1495
+ * }
1496
+ *
1497
+ * @example
1498
+ * // Case-insensitive deserialization
1499
+ * const Colors = createBrandedEnum('colors', {
1500
+ * Red: 'red',
1501
+ * Green: 'green',
1502
+ * Blue: 'blue',
1503
+ * } as const);
1504
+ *
1505
+ * const caseInsensitiveSerializer = enumSerializer(Colors, {
1506
+ * deserialize: (value) => value.toLowerCase(),
1507
+ * });
1508
+ *
1509
+ * caseInsensitiveSerializer.deserialize('RED'); // { success: true, value: 'red' }
1510
+ * caseInsensitiveSerializer.deserialize('Red'); // { success: true, value: 'red' }
1511
+ * caseInsensitiveSerializer.deserialize('red'); // { success: true, value: 'red' }
1512
+ */ export function enumSerializer(enumObj, options = {}) {
1513
+ // Validate that enumObj is a branded enum
1514
+ if (!isBrandedEnum(enumObj)) {
1515
+ throw new Error('enumSerializer requires a branded enum');
1516
+ }
1517
+ const enumId = enumObj[ENUM_ID];
1518
+ const values = enumObj[ENUM_VALUES];
1519
+ const validValues = Array.from(values).sort();
1520
+ const { serialize: serializeTransform, deserialize: deserializeTransform } = options;
1521
+ return {
1522
+ enumObj,
1523
+ enumId,
1524
+ serialize (value) {
1525
+ // Apply custom transform if provided
1526
+ if (serializeTransform) {
1527
+ return serializeTransform(value);
1528
+ }
1529
+ return value;
1530
+ },
1531
+ deserialize (value) {
1532
+ // Check if value is a string
1533
+ if (typeof value !== 'string') {
1534
+ const valueType = value === null ? 'null' : typeof value;
1535
+ return {
1536
+ success: false,
1537
+ error: {
1538
+ message: `Expected a string value, received ${valueType}`,
1539
+ input: value,
1540
+ enumId,
1541
+ validValues
1542
+ }
1543
+ };
1544
+ }
1545
+ // Apply custom deserialize transform if provided
1546
+ let transformedValue = value;
1547
+ if (deserializeTransform) {
1548
+ try {
1549
+ transformedValue = deserializeTransform(value);
1550
+ } catch (e) {
1551
+ return {
1552
+ success: false,
1553
+ error: {
1554
+ message: `Deserialize transform failed: ${e instanceof Error ? e.message : String(e)}`,
1555
+ input: value,
1556
+ enumId,
1557
+ validValues
1558
+ }
1559
+ };
1560
+ }
1561
+ }
1562
+ // Validate against the enum
1563
+ if (!values.has(transformedValue)) {
1564
+ return {
1565
+ success: false,
1566
+ error: {
1567
+ message: `Value "${transformedValue}" is not a member of enum "${enumId}"`,
1568
+ input: value,
1569
+ enumId,
1570
+ validValues
1571
+ }
1572
+ };
1573
+ }
1574
+ return {
1575
+ success: true,
1576
+ value: transformedValue
1577
+ };
1578
+ },
1579
+ deserializeOrThrow (value) {
1580
+ const result = this.deserialize(value);
1581
+ if (!result.success) {
1582
+ throw new Error(result.error.message);
1583
+ }
1584
+ return result.value;
1585
+ }
1586
+ };
1587
+ }
1588
+
1589
+ //# sourceMappingURL=advanced.js.map