@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.
- package/LICENSE +21 -0
- package/README.md +756 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/accessors.js +75 -0
- package/dist/lib/accessors.js.map +1 -0
- package/dist/lib/advanced.js +1589 -0
- package/dist/lib/advanced.js.map +1 -0
- package/dist/lib/branded-enum.js +5 -0
- package/dist/lib/branded-enum.js.map +1 -0
- package/dist/lib/decorators.js +284 -0
- package/dist/lib/decorators.js.map +1 -0
- package/dist/lib/factory.js +77 -0
- package/dist/lib/factory.js.map +1 -0
- package/dist/lib/guards.js +329 -0
- package/dist/lib/guards.js.map +1 -0
- package/dist/lib/merge.js +91 -0
- package/dist/lib/merge.js.map +1 -0
- package/dist/lib/registry.js +133 -0
- package/dist/lib/registry.js.map +1 -0
- package/dist/lib/types.js +23 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utils.js +161 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/package.json +120 -0
- package/package.json +110 -0
package/README.md
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
# @digitaldefiance/branded-enum
|
|
2
|
+
|
|
3
|
+
Runtime-identifiable enum-like types for TypeScript with zero runtime overhead.
|
|
4
|
+
|
|
5
|
+
## Why branded-enum?
|
|
6
|
+
|
|
7
|
+
Standard TypeScript enums are erased at compile time, making it impossible to determine which enum a string value originated from at runtime. This becomes problematic in large codebases with multiple libraries that may have overlapping string values.
|
|
8
|
+
|
|
9
|
+
**branded-enum** solves this by:
|
|
10
|
+
|
|
11
|
+
- Creating enum-like objects with embedded metadata for runtime identification
|
|
12
|
+
- Providing type guards to check if a value belongs to a specific enum
|
|
13
|
+
- Maintaining a global registry to track all branded enums across bundles
|
|
14
|
+
- Keeping values as raw strings for zero runtime overhead and serialization compatibility
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @digitaldefiance/branded-enum
|
|
20
|
+
# or
|
|
21
|
+
yarn add @digitaldefiance/branded-enum
|
|
22
|
+
# or
|
|
23
|
+
pnpm add @digitaldefiance/branded-enum
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { createBrandedEnum, isFromEnum, getEnumId } from '@digitaldefiance/branded-enum';
|
|
30
|
+
|
|
31
|
+
// Create a branded enum (use `as const` for literal type inference)
|
|
32
|
+
const Status = createBrandedEnum('status', {
|
|
33
|
+
Active: 'active',
|
|
34
|
+
Inactive: 'inactive',
|
|
35
|
+
Pending: 'pending',
|
|
36
|
+
} as const);
|
|
37
|
+
|
|
38
|
+
// Values are raw strings - no wrapper overhead
|
|
39
|
+
console.log(Status.Active); // 'active'
|
|
40
|
+
|
|
41
|
+
// Type guard with automatic type narrowing
|
|
42
|
+
function handleValue(value: unknown) {
|
|
43
|
+
if (isFromEnum(value, Status)) {
|
|
44
|
+
// value is narrowed to 'active' | 'inactive' | 'pending'
|
|
45
|
+
console.log('Valid status:', value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Runtime identification
|
|
50
|
+
console.log(getEnumId(Status)); // 'status'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
### Runtime Identification
|
|
56
|
+
|
|
57
|
+
Unlike standard TypeScript enums, branded enums carry metadata that enables runtime identification:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { createBrandedEnum, findEnumSources, getEnumById } from '@digitaldefiance/branded-enum';
|
|
61
|
+
|
|
62
|
+
const Colors = createBrandedEnum('colors', { Red: 'red', Blue: 'blue' } as const);
|
|
63
|
+
const Sizes = createBrandedEnum('sizes', { Small: 'small', Large: 'large' } as const);
|
|
64
|
+
|
|
65
|
+
// Find which enums contain a value
|
|
66
|
+
findEnumSources('red'); // ['colors']
|
|
67
|
+
|
|
68
|
+
// Retrieve enum by ID
|
|
69
|
+
const retrieved = getEnumById('colors');
|
|
70
|
+
console.log(retrieved === Colors); // true
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Type Guards
|
|
74
|
+
|
|
75
|
+
Validate values at runtime with automatic TypeScript type narrowing:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { createBrandedEnum, isFromEnum, assertFromEnum } from '@digitaldefiance/branded-enum';
|
|
79
|
+
|
|
80
|
+
const Priority = createBrandedEnum('priority', {
|
|
81
|
+
High: 'high',
|
|
82
|
+
Medium: 'medium',
|
|
83
|
+
Low: 'low',
|
|
84
|
+
} as const);
|
|
85
|
+
|
|
86
|
+
// Soft check - returns boolean
|
|
87
|
+
if (isFromEnum(userInput, Priority)) {
|
|
88
|
+
// userInput is typed as 'high' | 'medium' | 'low'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Hard check - throws on invalid value
|
|
92
|
+
const validated = assertFromEnum(userInput, Priority);
|
|
93
|
+
// Throws: 'Value "invalid" is not a member of enum "priority"'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Serialization Compatible
|
|
97
|
+
|
|
98
|
+
Branded enums serialize cleanly to JSON - metadata is stored in non-enumerable Symbol properties:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const Status = createBrandedEnum('status', { Active: 'active' } as const);
|
|
102
|
+
|
|
103
|
+
JSON.stringify(Status);
|
|
104
|
+
// '{"Active":"active"}' - no metadata pollution
|
|
105
|
+
|
|
106
|
+
Object.keys(Status); // ['Active']
|
|
107
|
+
Object.values(Status); // ['active']
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Cross-Bundle Registry
|
|
111
|
+
|
|
112
|
+
The global registry uses `globalThis`, ensuring all branded enums are tracked across different bundles, ESM/CJS modules, and even different instances of the library:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { getAllEnumIds, getEnumById } from '@digitaldefiance/branded-enum';
|
|
116
|
+
|
|
117
|
+
// List all registered enums
|
|
118
|
+
getAllEnumIds(); // ['status', 'colors', 'sizes', ...]
|
|
119
|
+
|
|
120
|
+
// Access any enum by ID
|
|
121
|
+
const enum = getEnumById('status');
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Enum Composition
|
|
125
|
+
|
|
126
|
+
Merge multiple enums into a new combined enum:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { createBrandedEnum, mergeEnums } from '@digitaldefiance/branded-enum';
|
|
130
|
+
|
|
131
|
+
const HttpSuccess = createBrandedEnum('http-success', {
|
|
132
|
+
OK: '200',
|
|
133
|
+
Created: '201',
|
|
134
|
+
} as const);
|
|
135
|
+
|
|
136
|
+
const HttpError = createBrandedEnum('http-error', {
|
|
137
|
+
BadRequest: '400',
|
|
138
|
+
NotFound: '404',
|
|
139
|
+
} as const);
|
|
140
|
+
|
|
141
|
+
const HttpCodes = mergeEnums('http-codes', HttpSuccess, HttpError);
|
|
142
|
+
// HttpCodes has: OK, Created, BadRequest, NotFound
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## API Reference
|
|
146
|
+
|
|
147
|
+
### Factory
|
|
148
|
+
|
|
149
|
+
#### `createBrandedEnum(enumId, values)`
|
|
150
|
+
|
|
151
|
+
Creates a branded enum with runtime metadata.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
function createBrandedEnum<T extends Record<string, string>>(
|
|
155
|
+
enumId: string,
|
|
156
|
+
values: T
|
|
157
|
+
): BrandedEnum<T>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- **enumId**: Unique identifier for this enum
|
|
161
|
+
- **values**: Object with key-value pairs (use `as const` for literal types)
|
|
162
|
+
- **Returns**: Frozen branded enum object
|
|
163
|
+
- **Throws**: `Error` if enumId is already registered
|
|
164
|
+
|
|
165
|
+
### Type Guards
|
|
166
|
+
|
|
167
|
+
#### `isFromEnum(value, enumObj)`
|
|
168
|
+
|
|
169
|
+
Checks if a value belongs to a branded enum.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
function isFromEnum<E extends BrandedEnum<Record<string, string>>>(
|
|
173
|
+
value: unknown,
|
|
174
|
+
enumObj: E
|
|
175
|
+
): value is BrandedEnumValue<E>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- Returns `true` with type narrowing if value is in the enum
|
|
179
|
+
- Returns `false` for non-string values or non-branded enum objects
|
|
180
|
+
|
|
181
|
+
#### `assertFromEnum(value, enumObj)`
|
|
182
|
+
|
|
183
|
+
Asserts a value belongs to a branded enum, throwing if not.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
function assertFromEnum<E extends BrandedEnum<Record<string, string>>>(
|
|
187
|
+
value: unknown,
|
|
188
|
+
enumObj: E
|
|
189
|
+
): BrandedEnumValue<E>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- **Returns**: The value with narrowed type
|
|
193
|
+
- **Throws**: `Error` if value is not in the enum
|
|
194
|
+
|
|
195
|
+
### Metadata Accessors
|
|
196
|
+
|
|
197
|
+
#### `getEnumId(enumObj)`
|
|
198
|
+
|
|
199
|
+
Gets the enum ID from a branded enum.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
function getEnumId(enumObj: unknown): string | undefined
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### `getEnumValues(enumObj)`
|
|
206
|
+
|
|
207
|
+
Gets all values from a branded enum as an array.
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
function getEnumValues<E extends BrandedEnum<Record<string, string>>>(
|
|
211
|
+
enumObj: E
|
|
212
|
+
): BrandedEnumValue<E>[] | undefined
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### `enumSize(enumObj)`
|
|
216
|
+
|
|
217
|
+
Gets the number of values in a branded enum.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
function enumSize(enumObj: unknown): number | undefined
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Registry Functions
|
|
224
|
+
|
|
225
|
+
#### `getAllEnumIds()`
|
|
226
|
+
|
|
227
|
+
Returns an array of all registered enum IDs.
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
function getAllEnumIds(): string[]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### `getEnumById(enumId)`
|
|
234
|
+
|
|
235
|
+
Gets a branded enum by its ID.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
function getEnumById(enumId: string): BrandedEnum<Record<string, string>> | undefined
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### `findEnumSources(value)`
|
|
242
|
+
|
|
243
|
+
Finds all enum IDs that contain a given value.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
function findEnumSources(value: string): string[]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Utility Functions
|
|
250
|
+
|
|
251
|
+
#### `hasValue(enumObj, value)`
|
|
252
|
+
|
|
253
|
+
Checks if a value exists in a branded enum (reverse lookup).
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
function hasValue<E extends BrandedEnum<Record<string, string>>>(
|
|
257
|
+
enumObj: E,
|
|
258
|
+
value: unknown
|
|
259
|
+
): value is BrandedEnumValue<E>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### `getKeyForValue(enumObj, value)`
|
|
263
|
+
|
|
264
|
+
Gets the key name for a value in a branded enum.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
function getKeyForValue<E extends BrandedEnum<Record<string, string>>>(
|
|
268
|
+
enumObj: E,
|
|
269
|
+
value: string
|
|
270
|
+
): keyof E | undefined
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### `isValidKey(enumObj, key)`
|
|
274
|
+
|
|
275
|
+
Checks if a key exists in a branded enum.
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
function isValidKey<E extends BrandedEnum<Record<string, string>>>(
|
|
279
|
+
enumObj: E,
|
|
280
|
+
key: unknown
|
|
281
|
+
): key is keyof E
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### `enumEntries(enumObj)`
|
|
285
|
+
|
|
286
|
+
Returns an iterator of [key, value] pairs.
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
function* enumEntries<E extends BrandedEnum<Record<string, string>>>(
|
|
290
|
+
enumObj: E
|
|
291
|
+
): IterableIterator<[keyof E, BrandedEnumValue<E>]>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Composition
|
|
295
|
+
|
|
296
|
+
#### `mergeEnums(newId, ...enums)`
|
|
297
|
+
|
|
298
|
+
Merges multiple branded enums into a new one.
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
function mergeEnums<T extends readonly BrandedEnum<Record<string, string>>[]>(
|
|
302
|
+
newId: string,
|
|
303
|
+
...enums: T
|
|
304
|
+
): BrandedEnum<Record<string, string>>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
- **Throws**: `Error` if duplicate keys are found across enums
|
|
308
|
+
- Duplicate values are allowed (intentional overlaps)
|
|
309
|
+
|
|
310
|
+
## Types
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// The branded enum type
|
|
314
|
+
type BrandedEnum<T extends Record<string, string>> = Readonly<T> & BrandedEnumMetadata;
|
|
315
|
+
|
|
316
|
+
// Extract value union from a branded enum
|
|
317
|
+
type BrandedEnumValue<E extends BrandedEnum<Record<string, string>>> =
|
|
318
|
+
E extends BrandedEnum<infer T> ? T[keyof T] : never;
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Use Cases
|
|
322
|
+
|
|
323
|
+
### i18n Key Management
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
const UserMessages = createBrandedEnum('user-messages', {
|
|
327
|
+
Welcome: 'user.welcome',
|
|
328
|
+
Goodbye: 'user.goodbye',
|
|
329
|
+
} as const);
|
|
330
|
+
|
|
331
|
+
const AdminMessages = createBrandedEnum('admin-messages', {
|
|
332
|
+
Welcome: 'admin.welcome', // Different value, same key name
|
|
333
|
+
} as const);
|
|
334
|
+
|
|
335
|
+
// Determine which translation namespace to use
|
|
336
|
+
function translate(key: string) {
|
|
337
|
+
const sources = findEnumSources(key);
|
|
338
|
+
if (sources.includes('user-messages')) {
|
|
339
|
+
return userTranslations[key];
|
|
340
|
+
}
|
|
341
|
+
if (sources.includes('admin-messages')) {
|
|
342
|
+
return adminTranslations[key];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### API Response Validation
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
const ApiStatus = createBrandedEnum('api-status', {
|
|
351
|
+
Success: 'success',
|
|
352
|
+
Error: 'error',
|
|
353
|
+
Pending: 'pending',
|
|
354
|
+
} as const);
|
|
355
|
+
|
|
356
|
+
function handleResponse(response: { status: unknown }) {
|
|
357
|
+
const status = assertFromEnum(response.status, ApiStatus);
|
|
358
|
+
// status is typed as 'success' | 'error' | 'pending'
|
|
359
|
+
|
|
360
|
+
switch (status) {
|
|
361
|
+
case ApiStatus.Success:
|
|
362
|
+
// TypeScript knows this is exhaustive
|
|
363
|
+
break;
|
|
364
|
+
case ApiStatus.Error:
|
|
365
|
+
break;
|
|
366
|
+
case ApiStatus.Pending:
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Plugin Systems
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// Core events
|
|
376
|
+
const CoreEvents = createBrandedEnum('core-events', {
|
|
377
|
+
Init: 'init',
|
|
378
|
+
Ready: 'ready',
|
|
379
|
+
} as const);
|
|
380
|
+
|
|
381
|
+
// Plugin events
|
|
382
|
+
const PluginEvents = createBrandedEnum('plugin-events', {
|
|
383
|
+
Load: 'plugin:load',
|
|
384
|
+
Unload: 'plugin:unload',
|
|
385
|
+
} as const);
|
|
386
|
+
|
|
387
|
+
// Combined for the event bus
|
|
388
|
+
const AllEvents = mergeEnums('all-events', CoreEvents, PluginEvents);
|
|
389
|
+
|
|
390
|
+
function emit(event: string) {
|
|
391
|
+
if (isFromEnum(event, CoreEvents)) {
|
|
392
|
+
handleCoreEvent(event);
|
|
393
|
+
} else if (isFromEnum(event, PluginEvents)) {
|
|
394
|
+
handlePluginEvent(event);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Advanced Features
|
|
400
|
+
|
|
401
|
+
The library includes powerful advanced features for complex use cases.
|
|
402
|
+
|
|
403
|
+
### Decorators
|
|
404
|
+
|
|
405
|
+
Runtime validation decorators for class properties that enforce enum membership.
|
|
406
|
+
|
|
407
|
+
#### `@EnumValue` - Property Validation
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
import { createBrandedEnum, EnumValue } from '@digitaldefiance/branded-enum';
|
|
411
|
+
|
|
412
|
+
const Status = createBrandedEnum('status', {
|
|
413
|
+
Active: 'active',
|
|
414
|
+
Inactive: 'inactive',
|
|
415
|
+
} as const);
|
|
416
|
+
|
|
417
|
+
class User {
|
|
418
|
+
@EnumValue(Status)
|
|
419
|
+
accessor status: string = Status.Active;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const user = new User();
|
|
423
|
+
user.status = Status.Active; // OK
|
|
424
|
+
user.status = 'invalid'; // Throws Error
|
|
425
|
+
|
|
426
|
+
// Optional and nullable support
|
|
427
|
+
class Config {
|
|
428
|
+
@EnumValue(Status, { optional: true })
|
|
429
|
+
accessor status: string | undefined;
|
|
430
|
+
|
|
431
|
+
@EnumValue(Status, { nullable: true })
|
|
432
|
+
accessor fallbackStatus: string | null = null;
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
#### `@EnumClass` - Usage Tracking
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
import { createBrandedEnum, EnumClass, getEnumConsumers, getConsumedEnums } from '@digitaldefiance/branded-enum';
|
|
440
|
+
|
|
441
|
+
const Status = createBrandedEnum('status', { Active: 'active' } as const);
|
|
442
|
+
const Priority = createBrandedEnum('priority', { High: 'high' } as const);
|
|
443
|
+
|
|
444
|
+
@EnumClass(Status, Priority)
|
|
445
|
+
class Task {
|
|
446
|
+
status = Status.Active;
|
|
447
|
+
priority = Priority.High;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Query enum usage
|
|
451
|
+
getEnumConsumers('status'); // ['Task']
|
|
452
|
+
getConsumedEnums('Task'); // ['status', 'priority']
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Enum Derivation
|
|
456
|
+
|
|
457
|
+
Create new enums from existing ones.
|
|
458
|
+
|
|
459
|
+
#### `enumSubset` - Select Keys
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { createBrandedEnum, enumSubset } from '@digitaldefiance/branded-enum';
|
|
463
|
+
|
|
464
|
+
const AllColors = createBrandedEnum('all-colors', {
|
|
465
|
+
Red: 'red', Green: 'green', Blue: 'blue', Yellow: 'yellow',
|
|
466
|
+
} as const);
|
|
467
|
+
|
|
468
|
+
const PrimaryColors = enumSubset('primary-colors', AllColors, ['Red', 'Blue', 'Yellow']);
|
|
469
|
+
// PrimaryColors has: Red, Blue, Yellow (no Green)
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### `enumExclude` - Remove Keys
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
import { createBrandedEnum, enumExclude } from '@digitaldefiance/branded-enum';
|
|
476
|
+
|
|
477
|
+
const Status = createBrandedEnum('status', {
|
|
478
|
+
Active: 'active', Inactive: 'inactive', Deprecated: 'deprecated',
|
|
479
|
+
} as const);
|
|
480
|
+
|
|
481
|
+
const CurrentStatuses = enumExclude('current-statuses', Status, ['Deprecated']);
|
|
482
|
+
// CurrentStatuses has: Active, Inactive
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
#### `enumMap` - Transform Values
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { createBrandedEnum, enumMap } from '@digitaldefiance/branded-enum';
|
|
489
|
+
|
|
490
|
+
const Status = createBrandedEnum('status', {
|
|
491
|
+
Active: 'active', Inactive: 'inactive',
|
|
492
|
+
} as const);
|
|
493
|
+
|
|
494
|
+
// Add prefix to all values
|
|
495
|
+
const PrefixedStatus = enumMap('prefixed-status', Status, (value) => `app.${value}`);
|
|
496
|
+
// PrefixedStatus.Active === 'app.active'
|
|
497
|
+
|
|
498
|
+
// Transform with key context
|
|
499
|
+
const VerboseStatus = enumMap('verbose-status', Status, (value, key) => `${key}: ${value}`);
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
#### `enumFromKeys` - Keys as Values
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
import { enumFromKeys } from '@digitaldefiance/branded-enum';
|
|
506
|
+
|
|
507
|
+
const Directions = enumFromKeys('directions', ['North', 'South', 'East', 'West'] as const);
|
|
508
|
+
// Equivalent to: { North: 'North', South: 'South', East: 'East', West: 'West' }
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Enum Analysis
|
|
512
|
+
|
|
513
|
+
Compare and analyze enums.
|
|
514
|
+
|
|
515
|
+
#### `enumDiff` - Compare Enums
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import { createBrandedEnum, enumDiff } from '@digitaldefiance/branded-enum';
|
|
519
|
+
|
|
520
|
+
const StatusV1 = createBrandedEnum('status-v1', {
|
|
521
|
+
Active: 'active', Inactive: 'inactive',
|
|
522
|
+
} as const);
|
|
523
|
+
|
|
524
|
+
const StatusV2 = createBrandedEnum('status-v2', {
|
|
525
|
+
Active: 'active', Inactive: 'disabled', Pending: 'pending',
|
|
526
|
+
} as const);
|
|
527
|
+
|
|
528
|
+
const diff = enumDiff(StatusV1, StatusV2);
|
|
529
|
+
// diff.onlyInFirst: []
|
|
530
|
+
// diff.onlyInSecond: [{ key: 'Pending', value: 'pending' }]
|
|
531
|
+
// diff.differentValues: [{ key: 'Inactive', firstValue: 'inactive', secondValue: 'disabled' }]
|
|
532
|
+
// diff.sameValues: [{ key: 'Active', value: 'active' }]
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
#### `enumIntersect` - Find Shared Values
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
import { createBrandedEnum, enumIntersect } from '@digitaldefiance/branded-enum';
|
|
539
|
+
|
|
540
|
+
const PrimaryColors = createBrandedEnum('primary', { Red: 'red', Blue: 'blue' } as const);
|
|
541
|
+
const WarmColors = createBrandedEnum('warm', { Red: 'red', Orange: 'orange' } as const);
|
|
542
|
+
|
|
543
|
+
const shared = enumIntersect(PrimaryColors, WarmColors);
|
|
544
|
+
// [{ value: 'red', enumIds: ['primary', 'warm'] }]
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Safe Parsing
|
|
548
|
+
|
|
549
|
+
Parse values without throwing errors.
|
|
550
|
+
|
|
551
|
+
#### `parseEnum` - With Default
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
import { createBrandedEnum, parseEnum } from '@digitaldefiance/branded-enum';
|
|
555
|
+
|
|
556
|
+
const Status = createBrandedEnum('status', { Active: 'active', Inactive: 'inactive' } as const);
|
|
557
|
+
|
|
558
|
+
const status = parseEnum(userInput, Status, Status.Active);
|
|
559
|
+
// Returns userInput if valid, otherwise Status.Active
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
#### `safeParseEnum` - Result Object
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
import { createBrandedEnum, safeParseEnum } from '@digitaldefiance/branded-enum';
|
|
566
|
+
|
|
567
|
+
const Status = createBrandedEnum('status', { Active: 'active', Inactive: 'inactive' } as const);
|
|
568
|
+
|
|
569
|
+
const result = safeParseEnum(userInput, Status);
|
|
570
|
+
if (result.success) {
|
|
571
|
+
console.log('Valid:', result.value);
|
|
572
|
+
} else {
|
|
573
|
+
console.log('Error:', result.error.message);
|
|
574
|
+
console.log('Valid values:', result.error.validValues);
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### Exhaustiveness Checking
|
|
579
|
+
|
|
580
|
+
Ensure all enum cases are handled in switch statements.
|
|
581
|
+
|
|
582
|
+
#### `exhaustive` - Generic Helper
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
import { createBrandedEnum, exhaustive } from '@digitaldefiance/branded-enum';
|
|
586
|
+
|
|
587
|
+
const Status = createBrandedEnum('status', {
|
|
588
|
+
Active: 'active', Inactive: 'inactive', Pending: 'pending',
|
|
589
|
+
} as const);
|
|
590
|
+
|
|
591
|
+
type StatusValue = typeof Status[keyof typeof Status];
|
|
592
|
+
|
|
593
|
+
function handleStatus(status: StatusValue): string {
|
|
594
|
+
switch (status) {
|
|
595
|
+
case Status.Active: return 'User is active';
|
|
596
|
+
case Status.Inactive: return 'User is inactive';
|
|
597
|
+
case Status.Pending: return 'User is pending';
|
|
598
|
+
default: return exhaustive(status); // TypeScript error if case missing
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
#### `exhaustiveGuard` - Enum-Specific Guard
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
import { createBrandedEnum, exhaustiveGuard } from '@digitaldefiance/branded-enum';
|
|
607
|
+
|
|
608
|
+
const Status = createBrandedEnum('status', { Active: 'active', Inactive: 'inactive' } as const);
|
|
609
|
+
const assertStatusExhaustive = exhaustiveGuard(Status);
|
|
610
|
+
|
|
611
|
+
function handleStatus(status: typeof Status[keyof typeof Status]): string {
|
|
612
|
+
switch (status) {
|
|
613
|
+
case Status.Active: return 'Active';
|
|
614
|
+
case Status.Inactive: return 'Inactive';
|
|
615
|
+
default: return assertStatusExhaustive(status);
|
|
616
|
+
// Error includes enum ID: 'Exhaustive check failed for enum "status"'
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### Schema Generation
|
|
622
|
+
|
|
623
|
+
Generate schemas for validation libraries.
|
|
624
|
+
|
|
625
|
+
#### `toJsonSchema` - JSON Schema
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
import { createBrandedEnum, toJsonSchema } from '@digitaldefiance/branded-enum';
|
|
629
|
+
|
|
630
|
+
const Status = createBrandedEnum('status', {
|
|
631
|
+
Active: 'active', Inactive: 'inactive',
|
|
632
|
+
} as const);
|
|
633
|
+
|
|
634
|
+
const schema = toJsonSchema(Status);
|
|
635
|
+
// {
|
|
636
|
+
// $schema: 'http://json-schema.org/draft-07/schema#',
|
|
637
|
+
// title: 'status',
|
|
638
|
+
// description: 'Enum values for status',
|
|
639
|
+
// type: 'string',
|
|
640
|
+
// enum: ['active', 'inactive']
|
|
641
|
+
// }
|
|
642
|
+
|
|
643
|
+
// Custom options
|
|
644
|
+
const customSchema = toJsonSchema(Status, {
|
|
645
|
+
title: 'User Status',
|
|
646
|
+
description: 'The current status of a user',
|
|
647
|
+
schemaVersion: '2020-12',
|
|
648
|
+
});
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
#### `toZodSchema` - Zod-Compatible Definition
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
import { createBrandedEnum, toZodSchema } from '@digitaldefiance/branded-enum';
|
|
655
|
+
import { z } from 'zod';
|
|
656
|
+
|
|
657
|
+
const Status = createBrandedEnum('status', {
|
|
658
|
+
Active: 'active', Inactive: 'inactive',
|
|
659
|
+
} as const);
|
|
660
|
+
|
|
661
|
+
const def = toZodSchema(Status);
|
|
662
|
+
const statusSchema = z.enum(def.values);
|
|
663
|
+
|
|
664
|
+
statusSchema.parse('active'); // 'active'
|
|
665
|
+
statusSchema.parse('invalid'); // throws ZodError
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### Serialization
|
|
669
|
+
|
|
670
|
+
Custom serialization/deserialization with transforms.
|
|
671
|
+
|
|
672
|
+
#### `enumSerializer` - Serializer Factory
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
import { createBrandedEnum, enumSerializer } from '@digitaldefiance/branded-enum';
|
|
676
|
+
|
|
677
|
+
const Status = createBrandedEnum('status', {
|
|
678
|
+
Active: 'active', Inactive: 'inactive',
|
|
679
|
+
} as const);
|
|
680
|
+
|
|
681
|
+
// Basic serializer
|
|
682
|
+
const serializer = enumSerializer(Status);
|
|
683
|
+
serializer.serialize(Status.Active); // 'active'
|
|
684
|
+
serializer.deserialize('active'); // { success: true, value: 'active' }
|
|
685
|
+
|
|
686
|
+
// With custom transforms (e.g., add prefix)
|
|
687
|
+
const prefixedSerializer = enumSerializer(Status, {
|
|
688
|
+
serialize: (value) => `status:${value}`,
|
|
689
|
+
deserialize: (value) => value.replace('status:', ''),
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
prefixedSerializer.serialize(Status.Active); // 'status:active'
|
|
693
|
+
prefixedSerializer.deserialize('status:active'); // { success: true, value: 'active' }
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Development Tooling
|
|
697
|
+
|
|
698
|
+
Debug and monitor enum usage.
|
|
699
|
+
|
|
700
|
+
#### `watchEnum` - Access Monitoring
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
import { createBrandedEnum, watchEnum } from '@digitaldefiance/branded-enum';
|
|
704
|
+
|
|
705
|
+
const Status = createBrandedEnum('status', { Active: 'active', Inactive: 'inactive' } as const);
|
|
706
|
+
|
|
707
|
+
const { watched, unwatch } = watchEnum(Status, (event) => {
|
|
708
|
+
console.log(`Accessed ${event.enumId}.${event.key} = ${event.value}`);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
watched.Active; // Logs: "Accessed status.Active = active"
|
|
712
|
+
unwatch(); // Stop watching
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### Utility Functions
|
|
716
|
+
|
|
717
|
+
#### `enumToRecord` - Strip Metadata
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
import { createBrandedEnum, enumToRecord } from '@digitaldefiance/branded-enum';
|
|
721
|
+
|
|
722
|
+
const Status = createBrandedEnum('status', { Active: 'active' } as const);
|
|
723
|
+
const plain = enumToRecord(Status);
|
|
724
|
+
// { Active: 'active' } - plain object, no Symbol metadata
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Compile-Time Types
|
|
728
|
+
|
|
729
|
+
TypeScript utility types for enhanced type safety.
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
import { createBrandedEnum, EnumKeys, EnumValues, ValidEnumValue, StrictEnumParam } from '@digitaldefiance/branded-enum';
|
|
733
|
+
|
|
734
|
+
const Status = createBrandedEnum('status', {
|
|
735
|
+
Active: 'active', Inactive: 'inactive',
|
|
736
|
+
} as const);
|
|
737
|
+
|
|
738
|
+
// Extract key union
|
|
739
|
+
type StatusKeys = EnumKeys<typeof Status>; // 'Active' | 'Inactive'
|
|
740
|
+
|
|
741
|
+
// Extract value union
|
|
742
|
+
type StatusValues = EnumValues<typeof Status>; // 'active' | 'inactive'
|
|
743
|
+
|
|
744
|
+
// Validate value at compile time
|
|
745
|
+
type Valid = ValidEnumValue<typeof Status, 'active'>; // 'active'
|
|
746
|
+
type Invalid = ValidEnumValue<typeof Status, 'unknown'>; // never
|
|
747
|
+
|
|
748
|
+
// Strict function parameters
|
|
749
|
+
function updateStatus(status: StrictEnumParam<typeof Status>): void {
|
|
750
|
+
// Only accepts 'active' | 'inactive'
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
## License
|
|
755
|
+
|
|
756
|
+
MIT © [Digital Defiance](https://github.com/Digital-Defiance)
|