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 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
+ [![npm](https://img.shields.io/npm/v/ts-safe-enum)](https://www.npmjs.com/package/ts-safe-enum)
6
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/ts-safe-enum)](https://bundlephobia.com/package/ts-safe-enum)
7
+ [![license](https://img.shields.io/npm/l/ts-safe-enum)](./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"]}
@@ -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 };
@@ -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
+ }