diff --git a/.changeset/clarify-union-to-tuple-order.md b/.changeset/clarify-union-to-tuple-order.md new file mode 100644 index 0000000..1fa309a --- /dev/null +++ b/.changeset/clarify-union-to-tuple-order.md @@ -0,0 +1,5 @@ +--- +"@ylfjuk-core/types": patch +--- + +Clarify that `UnionToTuple` element order is unspecified and unstable. It cannot predict `Object.values` order or recover duplicate values discarded by a union. diff --git a/.changeset/remove-enum-to-tuple.md b/.changeset/remove-enum-to-tuple.md new file mode 100644 index 0000000..0972538 --- /dev/null +++ b/.changeset/remove-enum-to-tuple.md @@ -0,0 +1,10 @@ +--- +"@ylfjuk-core/utils": patch +"@ylfjuk/core": patch +--- + +BREAKING CHANGE: remove `Enum.toTuple`, its `enumToTuple` alias, and the otherwise empty `Enum` namespace. Imports of these APIs must be removed. + +The helper's ordered-tuple guarantee was unsound: object types and unions cannot encode runtime insertion order, and unions discard duplicate values even though `Object.values` retains them. The claimed tuple could therefore have incorrect positions and length. + +Replace `Enum.toTuple(enumLikeObject)` or `enumToTuple(enumLikeObject)` with `Object.values(enumLikeObject)` for an array without fixed positional typing. When exact positions matter, define an explicit tuple as the source of truth, for example `const names = ["bob", "bert", "bob"] as const`. See the [migration guide](https://github.com/YLfjuk/core/blob/main/packages/utils/README.md#migrate-from-enumtotuple). diff --git a/packages/types/src/union-to-tuple.ts b/packages/types/src/union-to-tuple.ts index 79b0df0..858530e 100644 --- a/packages/types/src/union-to-tuple.ts +++ b/packages/types/src/union-to-tuple.ts @@ -1,15 +1,22 @@ import type { LastOfUnion } from "./last-of-union.js"; /** - * Converts a union into an ordered tuple. + * Converts a union into a tuple with unspecified, unstable element order. + * + * @remarks + * Do not rely on a particular order or use this type to predict `Object.values` + * order. Unions do not preserve insertion order or duplicate values. * * @see {@link LastOfUnion} * * @example - * type Enum = "bert" | "berta" | "bob"; + * import type { UnionToTuple } from "@ylfjuk-core/types"; + * + * type Name = "bert" | "berta" | "bob"; * - * type EnumAsTuple = UnionToTuple; // ["bert", "berta", "bob"] - * type IsEnumAsTuple = IsTuple; // true + * type NameTuple = UnionToTuple; // Element order is unspecified. + * type Members = NameTuple[number]; // "bert" | "berta" | "bob" + * type Length = NameTuple["length"]; // 3 * * @since 0.0.18 */ diff --git a/packages/utils/README.md b/packages/utils/README.md index fe17903..e7d408d 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -14,6 +14,38 @@ This package is ESM-only and targets ES2024. import { isDefined } from "@ylfjuk-core/utils"; ``` +## Migrate from Enum.toTuple + +BREAKING CHANGE: `Enum.toTuple`, its `enumToTuple` alias, and the `Enum` namespace have been removed from `@ylfjuk-core/utils` and the `@ylfjuk/core` re-exports. `Enum` contained no other helpers. + +The helper's ordered-tuple guarantee was unsound. Object types and unions cannot encode runtime insertion order, and unions discard duplicate values. For example, two object keys with the value `"bob"` produce two array entries at runtime but only one `"bob"` union member, so the claimed tuple could have the wrong length as well as the wrong positions. + +Replace calls to `Enum.toTuple(Name)` or `enumToTuple(Name)` with `Object.values(Name)` when you need an array: + +```ts +const Name = { + Bob: "bob", + Bert: "bert", + AlsoBob: "bob", +} as const; + +const names = Object.values(Name); +// Type: ("bob" | "bert")[] +// Runtime: ["bob", "bert", "bob"] +``` + +Remove the old imports. The array retains duplicate values and follows JavaScript property enumeration order, but its type does not promise a fixed length or a value at each position. + +When exact positional typing matters, define an explicit tuple as the source of truth: + +```ts +const names = ["bob", "bert", "bob"] as const; +type Name = (typeof names)[number]; // "bob" | "bert" +// typeof names: readonly ["bob", "bert", "bob"] +``` + +`UnionToTuple` remains public. Its element order is unspecified and unstable; it cannot predict `Object.values` order or recover duplicates discarded by a union. Do not cast the array to `UnionToTuple` to restore the removed guarantee. + ## Development From the repository root, use Node.js 22.18.0 or newer and run `pnpm run build`, `pnpm run test`, and `pnpm run test:types`. diff --git a/packages/utils/__tests__/enum/to-tuple.spec.ts b/packages/utils/__tests__/enum/to-tuple.spec.ts deleted file mode 100644 index 04c6534..0000000 --- a/packages/utils/__tests__/enum/to-tuple.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, expectTypeOf, it } from "vitest"; - -import { Enum } from "../../src/enum"; - -describe("enum/toTuple", () => { - it("should convert the enum's values into a tuple", () => { - const Name = { - Bob: "bob", - Bert: "bert", - Berta: "berta", - } as const; - - const Names = Enum.toTuple(Name); - - type Actual = typeof Names; - type Expected = ["bob", "bert", "berta"]; - - expectTypeOf().toEqualTypeOf(); - expect(Names).toStrictEqual(["bob", "bert", "berta"]); - expect(Names).not.toStrictEqual(["bert", "bob", "berta"]); - }); -}); diff --git a/packages/utils/src/enum/Enum.ts b/packages/utils/src/enum/Enum.ts deleted file mode 100644 index 9554a99..0000000 --- a/packages/utils/src/enum/Enum.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./to-tuple"; diff --git a/packages/utils/src/enum/index.ts b/packages/utils/src/enum/index.ts deleted file mode 100644 index 9384740..0000000 --- a/packages/utils/src/enum/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Utility module for enums. - * - * @since 0.0.10 - */ -export * as Enum from "./Enum"; - -export { toTuple as enumToTuple } from "./to-tuple"; diff --git a/packages/utils/src/enum/to-tuple.ts b/packages/utils/src/enum/to-tuple.ts deleted file mode 100644 index 88ad39d..0000000 --- a/packages/utils/src/enum/to-tuple.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { UnionToTuple, ValueOf } from "@ylfjuk-core/types"; - -/** - * Converts an enum-like object into an ordered tuple. - * - * @see {@link UnionToTuple} - * @see {@link ValueOf} - * - * @example - * const Name = { - * Bob: "bob", - * Bert: "bert", - * Berta: "berta" - * } as const; - * - * const Names = Enum.toTuple(Name); // ["bob", "bert", "berta"] - * - * @since 0.0.10 - */ -export const toTuple = >(e: E) => - Object.values(e) as UnionToTuple>; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index a9953cb..506d9d1 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,7 +1,6 @@ export * from "./arr"; export * from "./assert-never"; export * from "./bool"; -export * from "./enum"; export * from "./infinite-object"; export * from "./is-defined"; export * from "./json";