ts-safe-enum 1.0.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.
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maryan Mats
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# ts-safe-enum
|
|
2
|
+
|
|
3
|
+
A tiny, type-safe alternative to TypeScript enums. Built on plain objects with full inference — no `as const` needed, no runtime surprises.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/ts-safe-enum)
|
|
6
|
+
[](https://bundlephobia.com/package/ts-safe-enum)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
## Why?
|
|
10
|
+
|
|
11
|
+
TypeScript enums look simple but hide real problems:
|
|
12
|
+
|
|
13
|
+
- **Numeric enums** generate reverse mappings and accept any number at runtime
|
|
14
|
+
- **String enums** behave like nominal types — they reject identical string values from APIs and localStorage
|
|
15
|
+
- **`const enum`** gets inlined away and breaks with declaration files, barrel exports, and modern toolchains
|
|
16
|
+
- **All enums** compile to IIFEs that bundlers can't tree-shake
|
|
17
|
+
|
|
18
|
+
The community solution is `as const` objects with union types. But working with them requires boilerplate — `Object.values()` loses type info, there's no type guard, no validation, no reverse lookup.
|
|
19
|
+
|
|
20
|
+
`ts-safe-enum` gives you a single function that turns a plain object into a fully typed enum with utilities — **zero dependencies, under 1KB**.
|
|
21
|
+
|
|
22
|
+
> **Read the full philosophy:** [Why I Stopped Using Enums in TypeScript](https://maryanmats.dev/blog/why-i-stopped-using-enums-in-typescript)
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install ts-safe-enum
|
|
28
|
+
# or
|
|
29
|
+
pnpm add ts-safe-enum
|
|
30
|
+
# or
|
|
31
|
+
yarn add ts-safe-enum
|
|
32
|
+
# or
|
|
33
|
+
bun add ts-safe-enum
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { defineEnum, type InferValue } from 'ts-safe-enum';
|
|
40
|
+
|
|
41
|
+
const Status = defineEnum({
|
|
42
|
+
Active: 'active',
|
|
43
|
+
Inactive: 'inactive',
|
|
44
|
+
Pending: 'pending',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Extract the union type — no `as const` needed
|
|
48
|
+
type Status = InferValue<typeof Status>;
|
|
49
|
+
// 'active' | 'inactive' | 'pending'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### `defineEnum(definition)` — Create a type-safe enum
|
|
55
|
+
|
|
56
|
+
Accepts a plain object. Literal types are inferred automatically.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const Direction = defineEnum({
|
|
60
|
+
Up: 'up',
|
|
61
|
+
Down: 'down',
|
|
62
|
+
Left: 'left',
|
|
63
|
+
Right: 'right',
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### `.values()` — Get all values as a typed array
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
Status.values();
|
|
73
|
+
// readonly ['active', 'inactive', 'pending']
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `.keys()` — Get all keys as a typed array
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
Status.keys();
|
|
80
|
+
// readonly ['Active', 'Inactive', 'Pending']
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `.entries()` — Get all key-value pairs
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
Status.entries();
|
|
87
|
+
// readonly [['Active', 'active'], ['Inactive', 'inactive'], ['Pending', 'pending']]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### `.is(value)` — Type guard for unknown values
|
|
93
|
+
|
|
94
|
+
Narrows `unknown` to the enum's value union:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const input: unknown = getUserInput();
|
|
98
|
+
|
|
99
|
+
if (Status.is(input)) {
|
|
100
|
+
// input is narrowed to 'active' | 'inactive' | 'pending'
|
|
101
|
+
console.log(`Valid status: ${input}`);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `.parse(value)` — Validate with a Result
|
|
106
|
+
|
|
107
|
+
Returns `{ ok: true, value }` or `{ ok: false, error }`:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const result = Status.parse(apiResponse.status);
|
|
111
|
+
|
|
112
|
+
if (result.ok) {
|
|
113
|
+
console.log(result.value); // typed as 'active' | 'inactive' | 'pending'
|
|
114
|
+
} else {
|
|
115
|
+
console.error(result.error);
|
|
116
|
+
// 'Invalid enum value: "unknown". Expected one of: active, inactive, pending'
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> If [`ts-safe-result`](https://www.npmjs.com/package/ts-safe-result) is installed, `.parse()` returns a full `Result` with `.map()`, `.match()`, and other utilities.
|
|
121
|
+
|
|
122
|
+
### `.keyOf(value)` — Reverse lookup
|
|
123
|
+
|
|
124
|
+
Get the key name for a given value:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
Status.keyOf('active'); // 'Active'
|
|
128
|
+
Status.keyOf('inactive'); // 'Inactive'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### Type Helpers
|
|
134
|
+
|
|
135
|
+
#### `InferValue<T>` — Extract the value union type
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
type Status = InferValue<typeof Status>;
|
|
139
|
+
// 'active' | 'inactive' | 'pending'
|
|
140
|
+
|
|
141
|
+
function setStatus(status: Status) { /* ... */ }
|
|
142
|
+
setStatus('active'); // ✅
|
|
143
|
+
setStatus('unknown'); // ❌ type error
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### `InferKey<T>` — Extract the key union type
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
type StatusKey = InferKey<typeof Status>;
|
|
150
|
+
// 'Active' | 'Inactive' | 'Pending'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Real-World Patterns
|
|
156
|
+
|
|
157
|
+
### Validating API responses
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const Role = defineEnum({
|
|
161
|
+
Admin: 'admin',
|
|
162
|
+
Editor: 'editor',
|
|
163
|
+
Viewer: 'viewer',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
type Role = InferValue<typeof Role>;
|
|
167
|
+
|
|
168
|
+
function handleUser(apiData: { role: string }) {
|
|
169
|
+
const result = Role.parse(apiData.role);
|
|
170
|
+
|
|
171
|
+
if (!result.ok) {
|
|
172
|
+
throw new Error(`Unknown role from API: ${apiData.role}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const role = result.value; // typed as 'admin' | 'editor' | 'viewer'
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Exhaustive switch statements
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const Theme = defineEnum({
|
|
183
|
+
Light: 'light',
|
|
184
|
+
Dark: 'dark',
|
|
185
|
+
System: 'system',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
type Theme = InferValue<typeof Theme>;
|
|
189
|
+
|
|
190
|
+
function getBackground(theme: Theme): string {
|
|
191
|
+
switch (theme) {
|
|
192
|
+
case Theme.definition.Light: return '#ffffff';
|
|
193
|
+
case Theme.definition.Dark: return '#1a1a1a';
|
|
194
|
+
case Theme.definition.System: return getSystemBackground();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Iterating over values
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const Priority = defineEnum({
|
|
203
|
+
Low: 1,
|
|
204
|
+
Medium: 2,
|
|
205
|
+
High: 3,
|
|
206
|
+
Critical: 4,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Render a dropdown
|
|
210
|
+
Priority.entries().map(([label, value]) => (
|
|
211
|
+
<option key={value} value={value}>{label}</option>
|
|
212
|
+
));
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### With `ts-safe-result`
|
|
216
|
+
|
|
217
|
+
When both packages are installed, `.parse()` returns a full `Result`:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { defineEnum } from 'ts-safe-enum';
|
|
221
|
+
|
|
222
|
+
const Color = defineEnum({
|
|
223
|
+
Red: 'red',
|
|
224
|
+
Green: 'green',
|
|
225
|
+
Blue: 'blue',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
Color.parse(userInput).match({
|
|
229
|
+
ok: color => applyColor(color),
|
|
230
|
+
err: message => showError(message),
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Comparison
|
|
235
|
+
|
|
236
|
+
| Feature | ts-safe-enum | TS enum | `as const` + manual | ts-enum-util |
|
|
237
|
+
|---|---|---|---|---|
|
|
238
|
+
| Tree-shakable | Yes | No (IIFEs) | Yes | Partial |
|
|
239
|
+
| No reverse mapping | Yes | No (numeric) | Yes | No |
|
|
240
|
+
| Type guard (`.is()`) | Yes | No | Manual | Yes |
|
|
241
|
+
| Validation (`.parse()`) | Yes | No | Manual | No |
|
|
242
|
+
| Reverse lookup | Yes | Numeric only | Manual | Yes |
|
|
243
|
+
| `as const` required | No | N/A | Yes | N/A |
|
|
244
|
+
| Works with real enums | No | Yes | No | Yes |
|
|
245
|
+
| Bundle size | < 1KB | ~0 (inlined) | 0 | ~3KB |
|
|
246
|
+
|
|
247
|
+
## Part of the ts-safe family
|
|
248
|
+
|
|
249
|
+
- [`ts-safe-result`](https://www.npmjs.com/package/ts-safe-result) — Type-safe `Result<Value, Error>` for TypeScript
|
|
250
|
+
- [`ts-safe-enum`](https://www.npmjs.com/package/ts-safe-enum) — Type-safe enum alternative (you are here)
|
|
251
|
+
|
|
252
|
+
## Philosophy
|
|
253
|
+
|
|
254
|
+
1. **Plain objects over magic.** An enum should be a plain object that TypeScript understands completely — no code generation, no IIFEs, no reverse mappings.
|
|
255
|
+
2. **Inference over annotation.** You shouldn't need `as const`, type aliases, or helper types just to define a set of constants.
|
|
256
|
+
3. **Validate at boundaries.** APIs send strings, localStorage stores strings, URLs contain strings. `.is()` and `.parse()` handle the real world.
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
'use strict';function l(n){let s=Object.values(n),u=Object.keys(n),a=Object.entries(n),t=new Set(Object.values(n)),o=new Map;for(let[e,r]of Object.entries(n))o.set(r,e);return {definition:n,values(){return s},keys(){return u},entries(){return a},is(e){return t.has(e)},parse(e){if(t.has(e))return {ok:true,value:e};let r=s.map(String).join(", ");return {ok:false,error:`Invalid enum value: "${String(e)}". Expected one of: ${r}`}},keyOf(e){return o.get(e)}}}exports.defineEnum=l;//# sourceMappingURL=index.cjs.map
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["defineEnum","definition","allValues","allKeys","allEntries","valueSet","reverseLookup","key","value","validValues"],"mappings":"aAiFO,SAASA,CAAAA,CACdC,CAAAA,CACiB,CACjB,IAAMC,CAAAA,CAAY,MAAA,CAAO,MAAA,CAAOD,CAAU,CAAA,CACpCE,CAAAA,CAAU,MAAA,CAAO,IAAA,CAAKF,CAAU,EAChCG,CAAAA,CAAa,MAAA,CAAO,OAAA,CAAQH,CAAU,CAAA,CAEtCI,CAAAA,CAAW,IAAI,GAAA,CAAqB,MAAA,CAAO,MAAA,CAAOJ,CAAU,CAAC,CAAA,CAC7DK,CAAAA,CAAgB,IAAI,GAAA,CAE1B,IAAA,GAAW,CAACC,CAAAA,CAAKC,CAAK,CAAA,GAAK,MAAA,CAAO,OAAA,CAAQP,CAAU,CAAA,CAClDK,CAAAA,CAAc,GAAA,CAAIE,CAAAA,CAAOD,CAAG,CAAA,CAG9B,OAAO,CACL,UAAA,CAAAN,CAAAA,CAEA,MAAA,EAAS,CACP,OAAOC,CACT,CAAA,CAEA,IAAA,EAAO,CACL,OAAOC,CACT,CAAA,CAEA,OAAA,EAAU,CACR,OAAOC,CACT,CAAA,CAEA,EAAA,CAAGI,CAAAA,CAAqC,CACtC,OAAOH,CAAAA,CAAS,GAAA,CAAIG,CAAwB,CAC9C,CAAA,CAEA,KAAA,CAAMA,CAAAA,CAAyC,CAC7C,GAAIH,CAAAA,CAAS,GAAA,CAAIG,CAAwB,CAAA,CACvC,OAAO,CAAE,EAAA,CAAI,IAAA,CAAM,KAAA,CAAOA,CAAoB,CAAA,CAGhD,IAAMC,CAAAA,CAAcP,CAAAA,CAAU,IAAI,MAAM,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA,CACnD,OAAO,CACL,EAAA,CAAI,KAAA,CACJ,KAAA,CAAO,CAAA,qBAAA,EAAwB,MAAA,CAAOM,CAAK,CAAC,CAAA,oBAAA,EAAuBC,CAAW,CAAA,CAChF,CACF,CAAA,CAEA,KAAA,CAAMD,CAAAA,CAAmB,CACvB,OAAOF,CAAAA,CAAc,GAAA,CAAIE,CAAwB,CACnD,CACF,CACF","file":"index.cjs","sourcesContent":["/**\n * The type of values in an enum definition.\n * Extracted as a union of all values.\n */\nexport type InferValue<T extends EnumInstance<Record<string, string | number>>> =\n ReturnType<T[\"values\"]>[number];\n\n/**\n * The type of keys in an enum definition.\n * Extracted as a union of all keys.\n */\nexport type InferKey<T extends EnumInstance<Record<string, string | number>>> =\n ReturnType<T[\"keys\"]>[number];\n\n/** The result of `.parse()` — compatible with `ts-safe-result`'s Result shape. */\nexport type ParseResult<Value> =\n | { readonly ok: true; readonly value: Value }\n | { readonly ok: false; readonly error: string };\n\nexport interface EnumInstance<T extends Record<string, string | number>> {\n /** The original definition object. */\n readonly definition: T;\n\n /** Get all values as a readonly array. */\n values(): readonly T[keyof T][];\n\n /** Get all keys as a readonly array. */\n keys(): readonly (keyof T)[];\n\n /** Get all entries as readonly [key, value] pairs. */\n entries(): readonly (readonly [keyof T, T[keyof T]])[];\n\n /**\n * Type guard — check if an unknown value is a valid enum value.\n *\n * @example\n * if (Status.is(input)) {\n * // input is narrowed to 'active' | 'inactive' | 'pending'\n * }\n */\n is(value: unknown): value is T[keyof T];\n\n /**\n * Parse an unknown value into a typed enum value.\n * Returns `{ ok: true, value }` or `{ ok: false, error }`.\n *\n * The shape is compatible with `ts-safe-result` — you can wrap it\n * with `ok()`/`err()` for full chaining support.\n */\n parse(value: unknown): ParseResult<T[keyof T]>;\n\n /**\n * Get the key corresponding to a given value.\n * Returns undefined if the value is not in the enum.\n *\n * @example\n * Status.keyOf('active') // 'Active'\n */\n keyOf(value: T[keyof T]): keyof T | undefined;\n}\n\n/**\n * Define a type-safe enum from a plain object.\n * No `as const` needed — literal types are inferred automatically.\n *\n * @example\n * const Status = defineEnum({\n * Active: 'active',\n * Inactive: 'inactive',\n * Pending: 'pending',\n * });\n *\n * type Status = InferValue<typeof Status>;\n * // 'active' | 'inactive' | 'pending'\n *\n * Status.values() // ['active', 'inactive', 'pending']\n * Status.keys() // ['Active', 'Inactive', 'Pending']\n * Status.is('active') // true (type guard)\n * Status.parse('xxx') // { ok: false, error: '...' }\n * Status.keyOf('active') // 'Active'\n */\nexport function defineEnum<const T extends Record<string, string | number>>(\n definition: T,\n): EnumInstance<T> {\n const allValues = Object.values(definition) as unknown as T[keyof T][];\n const allKeys = Object.keys(definition) as unknown as (keyof T)[];\n const allEntries = Object.entries(definition) as unknown as (readonly [keyof T, T[keyof T]])[];\n\n const valueSet = new Set<string | number>(Object.values(definition));\n const reverseLookup = new Map<string | number, string>();\n\n for (const [key, value] of Object.entries(definition)) {\n reverseLookup.set(value, key);\n }\n\n return {\n definition,\n\n values() {\n return allValues;\n },\n\n keys() {\n return allKeys;\n },\n\n entries() {\n return allEntries;\n },\n\n is(value: unknown): value is T[keyof T] {\n return valueSet.has(value as string | number);\n },\n\n parse(value: unknown): ParseResult<T[keyof T]> {\n if (valueSet.has(value as string | number)) {\n return { ok: true, value: value as T[keyof T] };\n }\n\n const validValues = allValues.map(String).join(\", \");\n return {\n ok: false,\n error: `Invalid enum value: \"${String(value)}\". Expected one of: ${validValues}`,\n };\n },\n\n keyOf(value: T[keyof T]) {\n return reverseLookup.get(value as string | number) as keyof T | undefined;\n },\n };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type of values in an enum definition.
|
|
3
|
+
* Extracted as a union of all values.
|
|
4
|
+
*/
|
|
5
|
+
type InferValue<T extends EnumInstance<Record<string, string | number>>> = ReturnType<T["values"]>[number];
|
|
6
|
+
/**
|
|
7
|
+
* The type of keys in an enum definition.
|
|
8
|
+
* Extracted as a union of all keys.
|
|
9
|
+
*/
|
|
10
|
+
type InferKey<T extends EnumInstance<Record<string, string | number>>> = ReturnType<T["keys"]>[number];
|
|
11
|
+
/** The result of `.parse()` — compatible with `ts-safe-result`'s Result shape. */
|
|
12
|
+
type ParseResult<Value> = {
|
|
13
|
+
readonly ok: true;
|
|
14
|
+
readonly value: Value;
|
|
15
|
+
} | {
|
|
16
|
+
readonly ok: false;
|
|
17
|
+
readonly error: string;
|
|
18
|
+
};
|
|
19
|
+
interface EnumInstance<T extends Record<string, string | number>> {
|
|
20
|
+
/** The original definition object. */
|
|
21
|
+
readonly definition: T;
|
|
22
|
+
/** Get all values as a readonly array. */
|
|
23
|
+
values(): readonly T[keyof T][];
|
|
24
|
+
/** Get all keys as a readonly array. */
|
|
25
|
+
keys(): readonly (keyof T)[];
|
|
26
|
+
/** Get all entries as readonly [key, value] pairs. */
|
|
27
|
+
entries(): readonly (readonly [keyof T, T[keyof T]])[];
|
|
28
|
+
/**
|
|
29
|
+
* Type guard — check if an unknown value is a valid enum value.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* if (Status.is(input)) {
|
|
33
|
+
* // input is narrowed to 'active' | 'inactive' | 'pending'
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
is(value: unknown): value is T[keyof T];
|
|
37
|
+
/**
|
|
38
|
+
* Parse an unknown value into a typed enum value.
|
|
39
|
+
* Returns `{ ok: true, value }` or `{ ok: false, error }`.
|
|
40
|
+
*
|
|
41
|
+
* The shape is compatible with `ts-safe-result` — you can wrap it
|
|
42
|
+
* with `ok()`/`err()` for full chaining support.
|
|
43
|
+
*/
|
|
44
|
+
parse(value: unknown): ParseResult<T[keyof T]>;
|
|
45
|
+
/**
|
|
46
|
+
* Get the key corresponding to a given value.
|
|
47
|
+
* Returns undefined if the value is not in the enum.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* Status.keyOf('active') // 'Active'
|
|
51
|
+
*/
|
|
52
|
+
keyOf(value: T[keyof T]): keyof T | undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Define a type-safe enum from a plain object.
|
|
56
|
+
* No `as const` needed — literal types are inferred automatically.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* const Status = defineEnum({
|
|
60
|
+
* Active: 'active',
|
|
61
|
+
* Inactive: 'inactive',
|
|
62
|
+
* Pending: 'pending',
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* type Status = InferValue<typeof Status>;
|
|
66
|
+
* // 'active' | 'inactive' | 'pending'
|
|
67
|
+
*
|
|
68
|
+
* Status.values() // ['active', 'inactive', 'pending']
|
|
69
|
+
* Status.keys() // ['Active', 'Inactive', 'Pending']
|
|
70
|
+
* Status.is('active') // true (type guard)
|
|
71
|
+
* Status.parse('xxx') // { ok: false, error: '...' }
|
|
72
|
+
* Status.keyOf('active') // 'Active'
|
|
73
|
+
*/
|
|
74
|
+
declare function defineEnum<const T extends Record<string, string | number>>(definition: T): EnumInstance<T>;
|
|
75
|
+
|
|
76
|
+
export { type EnumInstance, type InferKey, type InferValue, type ParseResult, defineEnum };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type of values in an enum definition.
|
|
3
|
+
* Extracted as a union of all values.
|
|
4
|
+
*/
|
|
5
|
+
type InferValue<T extends EnumInstance<Record<string, string | number>>> = ReturnType<T["values"]>[number];
|
|
6
|
+
/**
|
|
7
|
+
* The type of keys in an enum definition.
|
|
8
|
+
* Extracted as a union of all keys.
|
|
9
|
+
*/
|
|
10
|
+
type InferKey<T extends EnumInstance<Record<string, string | number>>> = ReturnType<T["keys"]>[number];
|
|
11
|
+
/** The result of `.parse()` — compatible with `ts-safe-result`'s Result shape. */
|
|
12
|
+
type ParseResult<Value> = {
|
|
13
|
+
readonly ok: true;
|
|
14
|
+
readonly value: Value;
|
|
15
|
+
} | {
|
|
16
|
+
readonly ok: false;
|
|
17
|
+
readonly error: string;
|
|
18
|
+
};
|
|
19
|
+
interface EnumInstance<T extends Record<string, string | number>> {
|
|
20
|
+
/** The original definition object. */
|
|
21
|
+
readonly definition: T;
|
|
22
|
+
/** Get all values as a readonly array. */
|
|
23
|
+
values(): readonly T[keyof T][];
|
|
24
|
+
/** Get all keys as a readonly array. */
|
|
25
|
+
keys(): readonly (keyof T)[];
|
|
26
|
+
/** Get all entries as readonly [key, value] pairs. */
|
|
27
|
+
entries(): readonly (readonly [keyof T, T[keyof T]])[];
|
|
28
|
+
/**
|
|
29
|
+
* Type guard — check if an unknown value is a valid enum value.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* if (Status.is(input)) {
|
|
33
|
+
* // input is narrowed to 'active' | 'inactive' | 'pending'
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
is(value: unknown): value is T[keyof T];
|
|
37
|
+
/**
|
|
38
|
+
* Parse an unknown value into a typed enum value.
|
|
39
|
+
* Returns `{ ok: true, value }` or `{ ok: false, error }`.
|
|
40
|
+
*
|
|
41
|
+
* The shape is compatible with `ts-safe-result` — you can wrap it
|
|
42
|
+
* with `ok()`/`err()` for full chaining support.
|
|
43
|
+
*/
|
|
44
|
+
parse(value: unknown): ParseResult<T[keyof T]>;
|
|
45
|
+
/**
|
|
46
|
+
* Get the key corresponding to a given value.
|
|
47
|
+
* Returns undefined if the value is not in the enum.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* Status.keyOf('active') // 'Active'
|
|
51
|
+
*/
|
|
52
|
+
keyOf(value: T[keyof T]): keyof T | undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Define a type-safe enum from a plain object.
|
|
56
|
+
* No `as const` needed — literal types are inferred automatically.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* const Status = defineEnum({
|
|
60
|
+
* Active: 'active',
|
|
61
|
+
* Inactive: 'inactive',
|
|
62
|
+
* Pending: 'pending',
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* type Status = InferValue<typeof Status>;
|
|
66
|
+
* // 'active' | 'inactive' | 'pending'
|
|
67
|
+
*
|
|
68
|
+
* Status.values() // ['active', 'inactive', 'pending']
|
|
69
|
+
* Status.keys() // ['Active', 'Inactive', 'Pending']
|
|
70
|
+
* Status.is('active') // true (type guard)
|
|
71
|
+
* Status.parse('xxx') // { ok: false, error: '...' }
|
|
72
|
+
* Status.keyOf('active') // 'Active'
|
|
73
|
+
*/
|
|
74
|
+
declare function defineEnum<const T extends Record<string, string | number>>(definition: T): EnumInstance<T>;
|
|
75
|
+
|
|
76
|
+
export { type EnumInstance, type InferKey, type InferValue, type ParseResult, defineEnum };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function l(n){let s=Object.values(n),u=Object.keys(n),a=Object.entries(n),t=new Set(Object.values(n)),o=new Map;for(let[e,r]of Object.entries(n))o.set(r,e);return {definition:n,values(){return s},keys(){return u},entries(){return a},is(e){return t.has(e)},parse(e){if(t.has(e))return {ok:true,value:e};let r=s.map(String).join(", ");return {ok:false,error:`Invalid enum value: "${String(e)}". Expected one of: ${r}`}},keyOf(e){return o.get(e)}}}export{l as defineEnum};//# sourceMappingURL=index.js.map
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["defineEnum","definition","allValues","allKeys","allEntries","valueSet","reverseLookup","key","value","validValues"],"mappings":"AAiFO,SAASA,CAAAA,CACdC,CAAAA,CACiB,CACjB,IAAMC,CAAAA,CAAY,MAAA,CAAO,MAAA,CAAOD,CAAU,CAAA,CACpCE,CAAAA,CAAU,MAAA,CAAO,IAAA,CAAKF,CAAU,EAChCG,CAAAA,CAAa,MAAA,CAAO,OAAA,CAAQH,CAAU,CAAA,CAEtCI,CAAAA,CAAW,IAAI,GAAA,CAAqB,MAAA,CAAO,MAAA,CAAOJ,CAAU,CAAC,CAAA,CAC7DK,CAAAA,CAAgB,IAAI,GAAA,CAE1B,IAAA,GAAW,CAACC,CAAAA,CAAKC,CAAK,CAAA,GAAK,MAAA,CAAO,OAAA,CAAQP,CAAU,CAAA,CAClDK,CAAAA,CAAc,GAAA,CAAIE,CAAAA,CAAOD,CAAG,CAAA,CAG9B,OAAO,CACL,UAAA,CAAAN,CAAAA,CAEA,MAAA,EAAS,CACP,OAAOC,CACT,CAAA,CAEA,IAAA,EAAO,CACL,OAAOC,CACT,CAAA,CAEA,OAAA,EAAU,CACR,OAAOC,CACT,CAAA,CAEA,EAAA,CAAGI,CAAAA,CAAqC,CACtC,OAAOH,CAAAA,CAAS,GAAA,CAAIG,CAAwB,CAC9C,CAAA,CAEA,KAAA,CAAMA,CAAAA,CAAyC,CAC7C,GAAIH,CAAAA,CAAS,GAAA,CAAIG,CAAwB,CAAA,CACvC,OAAO,CAAE,EAAA,CAAI,IAAA,CAAM,KAAA,CAAOA,CAAoB,CAAA,CAGhD,IAAMC,CAAAA,CAAcP,CAAAA,CAAU,IAAI,MAAM,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA,CACnD,OAAO,CACL,EAAA,CAAI,KAAA,CACJ,KAAA,CAAO,CAAA,qBAAA,EAAwB,MAAA,CAAOM,CAAK,CAAC,CAAA,oBAAA,EAAuBC,CAAW,CAAA,CAChF,CACF,CAAA,CAEA,KAAA,CAAMD,CAAAA,CAAmB,CACvB,OAAOF,CAAAA,CAAc,GAAA,CAAIE,CAAwB,CACnD,CACF,CACF","file":"index.js","sourcesContent":["/**\n * The type of values in an enum definition.\n * Extracted as a union of all values.\n */\nexport type InferValue<T extends EnumInstance<Record<string, string | number>>> =\n ReturnType<T[\"values\"]>[number];\n\n/**\n * The type of keys in an enum definition.\n * Extracted as a union of all keys.\n */\nexport type InferKey<T extends EnumInstance<Record<string, string | number>>> =\n ReturnType<T[\"keys\"]>[number];\n\n/** The result of `.parse()` — compatible with `ts-safe-result`'s Result shape. */\nexport type ParseResult<Value> =\n | { readonly ok: true; readonly value: Value }\n | { readonly ok: false; readonly error: string };\n\nexport interface EnumInstance<T extends Record<string, string | number>> {\n /** The original definition object. */\n readonly definition: T;\n\n /** Get all values as a readonly array. */\n values(): readonly T[keyof T][];\n\n /** Get all keys as a readonly array. */\n keys(): readonly (keyof T)[];\n\n /** Get all entries as readonly [key, value] pairs. */\n entries(): readonly (readonly [keyof T, T[keyof T]])[];\n\n /**\n * Type guard — check if an unknown value is a valid enum value.\n *\n * @example\n * if (Status.is(input)) {\n * // input is narrowed to 'active' | 'inactive' | 'pending'\n * }\n */\n is(value: unknown): value is T[keyof T];\n\n /**\n * Parse an unknown value into a typed enum value.\n * Returns `{ ok: true, value }` or `{ ok: false, error }`.\n *\n * The shape is compatible with `ts-safe-result` — you can wrap it\n * with `ok()`/`err()` for full chaining support.\n */\n parse(value: unknown): ParseResult<T[keyof T]>;\n\n /**\n * Get the key corresponding to a given value.\n * Returns undefined if the value is not in the enum.\n *\n * @example\n * Status.keyOf('active') // 'Active'\n */\n keyOf(value: T[keyof T]): keyof T | undefined;\n}\n\n/**\n * Define a type-safe enum from a plain object.\n * No `as const` needed — literal types are inferred automatically.\n *\n * @example\n * const Status = defineEnum({\n * Active: 'active',\n * Inactive: 'inactive',\n * Pending: 'pending',\n * });\n *\n * type Status = InferValue<typeof Status>;\n * // 'active' | 'inactive' | 'pending'\n *\n * Status.values() // ['active', 'inactive', 'pending']\n * Status.keys() // ['Active', 'Inactive', 'Pending']\n * Status.is('active') // true (type guard)\n * Status.parse('xxx') // { ok: false, error: '...' }\n * Status.keyOf('active') // 'Active'\n */\nexport function defineEnum<const T extends Record<string, string | number>>(\n definition: T,\n): EnumInstance<T> {\n const allValues = Object.values(definition) as unknown as T[keyof T][];\n const allKeys = Object.keys(definition) as unknown as (keyof T)[];\n const allEntries = Object.entries(definition) as unknown as (readonly [keyof T, T[keyof T]])[];\n\n const valueSet = new Set<string | number>(Object.values(definition));\n const reverseLookup = new Map<string | number, string>();\n\n for (const [key, value] of Object.entries(definition)) {\n reverseLookup.set(value, key);\n }\n\n return {\n definition,\n\n values() {\n return allValues;\n },\n\n keys() {\n return allKeys;\n },\n\n entries() {\n return allEntries;\n },\n\n is(value: unknown): value is T[keyof T] {\n return valueSet.has(value as string | number);\n },\n\n parse(value: unknown): ParseResult<T[keyof T]> {\n if (valueSet.has(value as string | number)) {\n return { ok: true, value: value as T[keyof T] };\n }\n\n const validValues = allValues.map(String).join(\", \");\n return {\n ok: false,\n error: `Invalid enum value: \"${String(value)}\". Expected one of: ${validValues}`,\n };\n },\n\n keyOf(value: T[keyof T]) {\n return reverseLookup.get(value as string | number) as keyof T | undefined;\n },\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ts-safe-enum",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A tiny, type-safe alternative to TypeScript enums. Built on 'as const' objects with full inference, no runtime surprises.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"enum",
|
|
34
|
+
"typescript",
|
|
35
|
+
"type-safe",
|
|
36
|
+
"as-const",
|
|
37
|
+
"const",
|
|
38
|
+
"union-type",
|
|
39
|
+
"discriminated-union",
|
|
40
|
+
"enum-alternative",
|
|
41
|
+
"object-enum",
|
|
42
|
+
"string-enum"
|
|
43
|
+
],
|
|
44
|
+
"author": "Maryan Mats <matsmaryan@gmail.com>",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/maryanmats/ts-safe-enum.git"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/maryanmats/ts-safe-enum#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/maryanmats/ts-safe-enum/issues"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"tsup": "^8.4.0",
|
|
56
|
+
"typescript": "^5.8.3",
|
|
57
|
+
"vitest": "^3.1.1"
|
|
58
|
+
}
|
|
59
|
+
}
|