From 030ce52bd2a03be291eb889df79f4ae42517ab87 Mon Sep 17 00:00:00 2001 From: benzaria Date: Wed, 11 Jun 2025 11:38:45 +0100 Subject: [PATCH] feat: move `Numeric` to options, improve options JsDoc & add new egde tests --- source/union-to-enum.d.ts | 71 ++++++++++++++++++++++++++++----------- test-d/union-to-enum.ts | 41 +++++++++++----------- 2 files changed, 73 insertions(+), 39 deletions(-) diff --git a/source/union-to-enum.d.ts b/source/union-to-enum.d.ts index 6b99660e68..05341d99bd 100644 --- a/source/union-to-enum.d.ts +++ b/source/union-to-enum.d.ts @@ -1,9 +1,10 @@ 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 {BuildTuple} from './internal/tuple.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. @@ -13,12 +14,43 @@ type UnionToEnumOptions = { The first numeric value to assign when using numeric indices. @default 1 + + @example + ``` + type E2 = 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; }; /** @@ -27,9 +59,9 @@ Converts a union or tuple of property keys (string, number, or symbol) into an * The keys are preserved, and their values are either: - Their own literal values (by default) -- Or numeric indices (`1`, `2`, ...) if `Numeric` is `true` +- Or numeric indices (`1`, `2`, ...) if {@link UnionToEnumOptions.numeric `numeric`} is `true` -By default, **Numeric Enums** start from **Index `1`**. See {@link UnionToEnumOptions} to change this behaviour. +By default, **numeric Enums** start from **Index `1`**. See {@link UnionToEnumOptions.startIndex `startIndex`} to change this behaviour. This is useful for creating strongly typed enums from a union/tuple of literals. @@ -40,10 +72,10 @@ import type {UnionToEnum} from 'type-fest'; type E1 = UnionToEnum<'A' | 'B' | 'C'>; //=> { A: 'A'; B: 'B'; C: 'C' } -type E2 = UnionToEnum<'X' | 'Y' | 'Z', true>; +type E2 = UnionToEnum<'X' | 'Y' | 'Z', {numeric: true}>; //=> { X: 1; Y: 2; Z: 3 } -type E3 = UnionToEnum<['Play', 'Pause', 'Stop'], true, {startIndex: 3}>; +type E3 = UnionToEnum<['Play', 'Pause', 'Stop'], {numeric: true; startIndex: 3}>; //=> { Play: 3; Pause: 4; Stop: 5 } type E4 = UnionToEnum<['some_key', 'another_key']>; @@ -84,31 +116,30 @@ const Template = createEnum(verb, resource); */ export type UnionToEnum< Keys extends PropertyKey | readonly PropertyKey[], - Numeric extends boolean = false, Options extends UnionToEnumOptions = {}, -> = ApplyDefaultOptions extends infer ResolvedOptions extends Required - ? IsNever extends true ? {} - : _UnionToEnum<[ - ...BuildTuple, // Shift the index - ...[Keys] extends [UnknownArray] ? Keys : UnionToTuple, - ], Numeric> - : never; +> = IsNever extends true ? {} + : _UnionToEnum< + [Keys] extends [UnknownArray] ? Keys : UnionToTuple, + ApplyDefaultOptions + >; /** Core type for {@link UnionToEnum}. */ type _UnionToEnum< Keys extends UnknownArray, - Numeric extends boolean, + Options extends Required, > = Simplify<{readonly [ K in keyof Keys as K extends `${number}` ? Keys[K] extends PropertyKey - ? Keys[K] - : never - : never - ]: Numeric extends true + ? IsLiteral extends true + ? 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}` - ? N - : never + ? Sum + : never // Not an index : Keys[K] }>; diff --git a/test-d/union-to-enum.ts b/test-d/union-to-enum.ts index c61eda90ae..d76b29f9cf 100644 --- a/test-d/union-to-enum.ts +++ b/test-d/union-to-enum.ts @@ -7,21 +7,21 @@ expectType>({1: 1, 2: 2, 3: 3, 4: 4} as const); // Tuple input expectType>({One: 'One', Two: 'Two'} as const); -expectType>({X: 1, Y: 2, Z: 3} as const); +expectType>({X: 1, Y: 2, Z: 3} as const); // Single element tuple expectType>({Only: 'Only'} as const); -expectType>({Only: 1} as const); +expectType>({Only: 1} as const); expectType>({0: 0} as const); -expectType>({0: 5} as const); +expectType>({0: 5} as const); // Tuple with numeric keys expectType>({1: 1, 2: 2, 3: 3} as const); -expectType>({1: 10, 2: 11, 3: 12} as const); +expectType>({1: 10, 2: 11, 3: 12} as const); // Mixed keys expectType>({a: 'a', 1: 1, b: 'b'} as const); -expectType>({a: 0, 1: 1, b: 2} as const); +expectType>({a: 0, 1: 1, b: 2} as const); // Symbol keys declare const sym1: unique symbol; @@ -31,20 +31,23 @@ expectType>({[sym1]: sym1, [sym2]: sym2} expectType>({[sym1]: sym1, [sym2]: sym2} as const); // Unordered union with numeric flag -expectType>({left: 1, right: 2, up: 3, down: 4} as const); +expectType>({left: 1, right: 2, up: 3, down: 4} as const); // Large union type BigUnion = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g'; expectType>({a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', g: 'g'} as const); // Non-literal input fallback -expectType>({} as Readonly>); -expectType>({} as Readonly>); -expectType>({} as Readonly>); - -expectType>({} as const); -expectType>({} as const); -expectType>({} as const); +expectType>({}); +expectType>({}); +expectType>({}); +expectType>({}); +expectType>({}); +expectType>({}); +expectType>({} as const); +expectType>({} as const); +expectType>({foo: 'foo'} as const); +expectType>({bar: 'bar'} as const); // Empty array cases expectType>({}); @@ -58,7 +61,7 @@ expectType>({}); const buttons = ['Play', 'Pause', 'Stop'] as const; expectType>({Play: 'Play', Pause: 'Pause', Stop: 'Stop'} as const); -expectType>({Play: 0, Pause: 1, Stop: 2} as const); +expectType>({Play: 0, Pause: 1, Stop: 2} as const); const level = ['DEBUG', 'INFO', 'ERROR', 'WARNING'] as const; @@ -68,7 +71,7 @@ expectType>({ ERROR: 'ERROR', WARNING: 'WARNING', } as const); -expectType>({ +expectType>({ DEBUG: 1, INFO: 2, ERROR: 3, @@ -100,10 +103,10 @@ expectType({ } as const); // Edge cases for startIndex -// expectType>({} as const); -expectType>({test: 100} as const); -expectType>({a: 0, b: 1} as const); +expectType>({x: -1} as const); +expectType>({x: -100, y: -99} as const); +expectType>({test: 100} as const); // Numeric edge cases expectType>({0: 0, [-1]: -1, 42: 42} as const); -expectType>({0: 1, [-5]: 2, 999: 3} as const); +expectType>({0: 1, [-5]: 2, 999: 3} as const);