Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a625258
Add `UnionToEnum` type and corresponding tests, and docs
benzaria Jun 7, 2025
c338ab4
Update example in `UnionToEnum` type to use more descriptive keys
benzaria Jun 7, 2025
a910dcb
Improve documentation for UnionToEnumOptions and related types
benzaria Jun 7, 2025
02f1a93
Add 'as const' to tests for `readonly` keyword
benzaria Jun 7, 2025
ef81023
feat: remove case transformation
benzaria Jun 9, 2025
ed6350d
Merge branch 'main' into UnionToEnum
benzaria Jun 9, 2025
767dbf9
Merge branch 'main' into UnionToEnum
benzaria Jun 10, 2025
40e029f
doc: add description in `readme`
benzaria Jun 10, 2025
e763095
doc: sanitize JsDoc & description
benzaria Jun 10, 2025
d54d526
feat: add extra Edge cases in tests
benzaria Jun 10, 2025
030ce52
feat: move `Numeric` to options, improve options JsDoc & add new egde…
benzaria Jun 11, 2025
eb8b785
Merge branch 'main' into UnionToEnum
benzaria Jun 18, 2025
e603f66
Merge branch 'main' into UnionToEnum
benzaria Sep 23, 2025
2471aef
Update union-to-enum.d.ts
sindresorhus Sep 27, 2025
b733afd
update docs
benzaria Sep 27, 2025
ba20563
Merge branch 'main' into UnionToEnum
benzaria Sep 27, 2025
d64d391
add `export {}` to EOL
benzaria Sep 27, 2025
dae00cf
Merge branch 'main' into UnionToEnum
benzaria Oct 9, 2025
7167d17
Update union-to-enum.d.ts
sindresorhus Oct 10, 2025
c7bad80
add more tests
benzaria Oct 19, 2025
7efbf20
fix: eliminate the possibility of passing a union and tuple keys at t…
benzaria Oct 20, 2025
bc8d651
Merge branch 'origin/UnionToEnum' into UnionToEnum
benzaria Oct 21, 2025
fcc9a08
docs: fix JsDoc examples
benzaria Oct 21, 2025
151e08b
add some test covering changes
benzaria Oct 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export type {IsFloat} from './source/is-float.d.ts';
export type {TupleToObject} from './source/tuple-to-object.d.ts';
export type {TupleToUnion} from './source/tuple-to-union.d.ts';
export type {UnionToTuple} from './source/union-to-tuple.d.ts';
export type {UnionToEnum} from './source/union-to-enum.d.ts';
export type {IntRange} from './source/int-range.d.ts';
export type {IntClosedRange} from './source/int-closed-range.d.ts';
export type {IsEqual} from './source/is-equal.d.ts';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ Click the type names for complete docs.
- [`ReadonlyTuple`](source/readonly-tuple.d.ts) - Create a type that represents a read-only tuple of the given type and length.
- [`TupleToUnion`](source/tuple-to-union.d.ts) - Convert a tuple/array into a union type of its elements.
- [`UnionToTuple`](source/union-to-tuple.d.ts) - Convert a union type into an unordered tuple type of its elements.
- [`UnionToEnum`](source/union-to-enum.d.ts) - Convert a union or tuple of property keys into an **Enum**.
- [`TupleToObject`](source/tuple-to-object.d.ts) - Transforms a tuple into an object, mapping each tuple index to its corresponding type as a key-value pair.
- [`TupleOf`](source/tuple-of.d.ts) - Creates a tuple type of the specified length with elements of the specified type.

Expand Down
151 changes: 151 additions & 0 deletions source/union-to-enum.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type {ApplyDefaultOptions} from './internal/object.d.ts';
import type {UnionToTuple} from './union-to-tuple.d.ts';
import type {UnknownArray} from './unknown-array.d.ts';
import type {IsLiteral} from './is-literal.d.ts';
import type {Simplify} from './simplify.d.ts';
import type {IsNever} from './is-never.d.ts';
import type {Sum} from './sum.d.ts';

/**
{@link UnionToEnum} Options.
*/
type UnionToEnumOptions = {
/**
The first numeric value to assign when using numeric indices.

@default 1

@example
```
type E1 = UnionToEnum<['Play', 'Pause', 'Stop'], {numeric: true}>;
//=> { Play: 1; Pause: 2; Stop: 3 }

type E2 = UnionToEnum<['Play', 'Pause', 'Stop'], {numeric: true; startIndex: 3}>;
//=> { Play: 3; Pause: 4; Stop: 5 }

type E3 = UnionToEnum<['Play', 'Pause', 'Stop'], {numeric: true; startIndex: -1}>;
//=> { Play: -1; Pause: 0; Stop: 1 }
```
*/
startIndex?: number;
/**
Whether to use numeric indices as values.

@default false

@example
```
type E1 = UnionToEnum<'X' | 'Y' | 'Z'>;
//=> { X: 'X'; Y: 'Y'; Z: 'Z' }

type E2 = UnionToEnum<'X' | 'Y' | 'Z', {numeric: true}>;
//=> { X: 1; Y: 2; Z: 3 }

type E3 = UnionToEnum<['Play', 'Pause', 'Stop'], {numeric: true; startIndex: 3}>;
//=> { Play: 3; Pause: 4; Stop: 5 }
```
*/
numeric?: boolean;
};

type DefaultUnionToEnumOptions = {
startIndex: 1;
numeric: false;
};

/**
Converts a union or tuple of property keys (string, number, or symbol) into an **Enum**.

The keys are preserved, and their values are either:

- Their own literal values (by default)
- Or numeric indices (`1`, `2`, ...) if {@link UnionToEnumOptions.numeric `numeric`} is `true`. Union ordering is not guaranteed.

By default, **numeric Enums** start from **Index `1`**. See {@link UnionToEnumOptions.startIndex `startIndex`} to change this behavior.

This is useful for creating strongly typed enums from a union/tuple of literals.

@example
```
import type {UnionToEnum} from 'type-fest';

type E1 = UnionToEnum<'A' | 'B' | 'C'>;
//=> { A: 'A'; B: 'B'; C: 'C' }

type E2 = UnionToEnum<'X' | 'Y' | 'Z', {numeric: true}>;
//=> { X: 1; Y: 2; Z: 3 }

type E3 = UnionToEnum<['Play', 'Pause', 'Stop'], {numeric: true; startIndex: 3}>;
//=> { Play: 3; Pause: 4; Stop: 5 }

type E4 = UnionToEnum<['some_key', 'another_key']>;
//=> { 'some_key': 'some_key'; 'another_key': 'another_key' }

type E5 = UnionToEnum<never>;
//=> {}
```

@example
```
import type {UnionToEnum, CamelCasedProperties} from 'type-fest';

const verb = ['write', 'read', 'delete'] as const;
const resource = ['file', 'folder', 'link'] as const;

declare function createEnum<
const T extends readonly string[],
const U extends readonly string[],
>(x: T, y: U): CamelCasedProperties<UnionToEnum<`${T[number]}_${U[number]}`>>;

const Template = createEnum(verb, resource);
//=> {
// writeFile: 'write_file',
// writeFolder: 'write_folder',
// writeLink: 'write_link',
// readFile: 'read_file',
// readFolder: 'read_folder',
// readLink: 'read_link',
// deleteFile: 'delete_file',
// deleteFolder: 'delete_folder',
// deleteLink: 'delete_link',
// }
```

@see UnionToTuple
@category Object
*/
export type UnionToEnum<
Keys extends (
[Keys] extends [PropertyKey]
? PropertyKey
: readonly PropertyKey[]
),
Options extends UnionToEnumOptions = {},
> = IsNever<Keys> extends true ? {}
: _UnionToEnum<
[Keys] extends [UnknownArray] ? Keys : UnionToTuple<Keys>,
ApplyDefaultOptions<UnionToEnumOptions, DefaultUnionToEnumOptions, Options>
>;

/**
Core type for {@link UnionToEnum}.
*/
type _UnionToEnum<
Keys extends UnknownArray,
Options extends Required<UnionToEnumOptions>,
> = Simplify<{readonly [
K in keyof Keys as K extends `${number}`
? Keys[K] extends PropertyKey
? IsLiteral<Keys[K]> extends true // TODO: update to accept template literals
? Keys[K]
: never // Not a literal
: never // Not a property key
: never // Not an index
]: Options['numeric'] extends true
? K extends `${infer N extends number}`
? Sum<N, Options['startIndex']>
: never // Not an index
: Keys[K]
}>;

export {};
1 change: 1 addition & 0 deletions source/union-to-tuple.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const petList = Object.keys(pets) as UnionToTuple<Pet>;
//=> ['dog', 'cat', 'snake']
```

@see UnionToEnum
@category Array
*/
export type UnionToTuple<T, L = LastOfUnion<T>> =
Expand Down
123 changes: 123 additions & 0 deletions test-d/union-to-enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {expectType} from 'tsd';
import type {CamelCasedProperties, UnionToEnum} from '../index.d.ts';

// Union input
expectType<UnionToEnum<'b' | 'a' | 'd' | 'c'>>({b: 'b', a: 'a', d: 'd', c: 'c'} as const);
expectType<UnionToEnum<3 | 2 | 4 | 1>>({1: 1, 2: 2, 3: 3, 4: 4} as const);

// Tuple input
expectType<UnionToEnum<['One', 'Two']>>({One: 'One', Two: 'Two'} as const);
expectType<UnionToEnum<['X', 'Y', 'Z'], {numeric: true}>>({X: 1, Y: 2, Z: 3} as const);
expectType<UnionToEnum<['A', 'B'] | ['C', 'D']>>({} as Readonly<{A: 'A'; B: 'B'} | {C: 'C'; D: 'D'}>);
expectType<UnionToEnum<['A', 'B'] | ['C', 'D'], {numeric: true}>>({} as Readonly<{A: 1; B: 2} | {C: 1; D: 2}>);

// Single element tuple
expectType<UnionToEnum<['Only']>>({Only: 'Only'} as const);
expectType<UnionToEnum<'Only', {numeric: true}>>({Only: 1} as const);
expectType<UnionToEnum<[0]>>({0: 0} as const);
expectType<UnionToEnum<[0], {numeric: true; startIndex: 5}>>({0: 5} as const);

// Tuple with numeric keys
expectType<UnionToEnum<[1, 2, 3]>>({1: 1, 2: 2, 3: 3} as const);
expectType<UnionToEnum<[1, 2, 3], {numeric: true; startIndex: 10}>>({1: 10, 2: 11, 3: 12} as const);

// Mixed keys
expectType<UnionToEnum<['a', 1, 'b']>>({a: 'a', 1: 1, b: 'b'} as const);
expectType<UnionToEnum<['a', 1, 'b'], {numeric: true; startIndex: 0}>>({a: 0, 1: 1, b: 2} as const);

// Symbol keys
declare const sym1: unique symbol;
declare const sym2: unique symbol;

expectType<UnionToEnum<typeof sym1 | typeof sym2>>({[sym1]: sym1, [sym2]: sym2} as const);
expectType<UnionToEnum<[typeof sym1, typeof sym2]>>({[sym1]: sym1, [sym2]: sym2} as const);

// Unordered union with numeric flag
expectType<UnionToEnum<'left' | 'right' | 'up' | 'down', {numeric: true}>>({left: 1, right: 2, up: 3, down: 4} as const);

// Large union
type BigUnion = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g';
expectType<UnionToEnum<BigUnion>>({a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', g: 'g'} as const);

// Symbols with numeric indices
expectType<UnionToEnum<[typeof sym1, typeof sym2], {numeric: true}>>({[sym1]: 1, [sym2]: 2} as const);

// Readonly tuple input
expectType<UnionToEnum<readonly ['a', 'b', 'c']>>({a: 'a', b: 'b', c: 'c'} as const);

// Non-literal input fallback
expectType<UnionToEnum<string>>({});
expectType<UnionToEnum<number>>({});
expectType<UnionToEnum<symbol>>({});
expectType<UnionToEnum<string[]>>({});
expectType<UnionToEnum<number[]>>({});
expectType<UnionToEnum<symbol[]>>({});
expectType<UnionToEnum<[string]>>({});
expectType<UnionToEnum<number | string>>({});
expectType<UnionToEnum<[string, 'foo']>>({foo: 'foo'} as const);
expectType<UnionToEnum<`foo${string}` | 'bar'>>({bar: 'bar'} as const);

// Empty array cases
expectType<UnionToEnum<[]>>({});
expectType<UnionToEnum<readonly []>>({});

// `never` / `any`
expectType<UnionToEnum<never>>({});
expectType<UnionToEnum<any>>({});

// Literal const arrays
const buttons = ['Play', 'Pause', 'Stop'] as const;

expectType<UnionToEnum<typeof buttons>>({Play: 'Play', Pause: 'Pause', Stop: 'Stop'} as const);
expectType<UnionToEnum<typeof buttons, {numeric: true; startIndex: 0}>>({Play: 0, Pause: 1, Stop: 2} as const);

const level = ['DEBUG', 'INFO', 'ERROR', 'WARNING'] as const;

expectType<UnionToEnum<typeof level>>({
DEBUG: 'DEBUG',
INFO: 'INFO',
ERROR: 'ERROR',
WARNING: 'WARNING',
} as const);
expectType<UnionToEnum<typeof level, {numeric: true}>>({
DEBUG: 1,
INFO: 2,
ERROR: 3,
WARNING: 4,
} as const);

// Dynamic Enum
const verb = ['write', 'read', 'delete'] as const;
const resource = ['file', 'folder', 'link'] as const;

declare function createEnum<
const T extends readonly string[],
const U extends readonly string[],
>(x: T, y: U): CamelCasedProperties<UnionToEnum<`${T[number]}_${U[number]}`>>;

const Template = createEnum(verb, resource);

expectType<typeof Template>({
writeFile: 'write_file',
writeFolder: 'write_folder',
writeLink: 'write_link',
readFile: 'read_file',
readFolder: 'read_folder',
readLink: 'read_link',
deleteFile: 'delete_file',
deleteFolder: 'delete_folder',
deleteLink: 'delete_link',
} as const);

// Edge cases for startIndex
expectType<UnionToEnum<['x'], {numeric: true; startIndex: -1}>>({x: -1} as const);
expectType<UnionToEnum<['x', 'y'], {numeric: true; startIndex: -100}>>({x: -100, y: -99} as const);
expectType<UnionToEnum<['test'], {numeric: true; startIndex: 100}>>({test: 100} as const);

// Numeric edge cases
expectType<UnionToEnum<0 | -1 | 42>>({0: 0, [-1]: -1, 42: 42} as const);
expectType<UnionToEnum<[0, -5, 999], {numeric: true}>>({0: 1, [-5]: 2, 999: 3} as const);

// @ts-expect-error no mixed input
type T = UnionToEnum<'A' | ['B']>;
type V = UnionToEnum<['A'] | ['B']>;