From c0a2a5a1fe337f9001e67e4c93f9e3280f9bdd44 Mon Sep 17 00:00:00 2001 From: YLfjuk Date: Sun, 18 Oct 2026 18:01:04 +0300 Subject: [PATCH 1/3] fix!: remove unsound enum-to-tuple helper (CORE-04) --- .changeset/remove-enum-to-tuple.md | 15 +++++++++ packages/core/README.md | 4 +++ packages/types/src/union-to-tuple.ts | 13 ++++++-- packages/utils/README.md | 32 +++++++++++++++++++ .../utils/__tests__/enum/to-tuple.spec.ts | 22 ------------- packages/utils/src/enum/Enum.ts | 1 - packages/utils/src/enum/index.ts | 8 ----- packages/utils/src/enum/to-tuple.ts | 21 ------------ packages/utils/src/index.ts | 1 - 9 files changed, 61 insertions(+), 56 deletions(-) create mode 100644 .changeset/remove-enum-to-tuple.md delete mode 100644 packages/utils/__tests__/enum/to-tuple.spec.ts delete mode 100644 packages/utils/src/enum/Enum.ts delete mode 100644 packages/utils/src/enum/index.ts delete mode 100644 packages/utils/src/enum/to-tuple.ts diff --git a/.changeset/remove-enum-to-tuple.md b/.changeset/remove-enum-to-tuple.md new file mode 100644 index 0000000..6128209 --- /dev/null +++ b/.changeset/remove-enum-to-tuple.md @@ -0,0 +1,15 @@ +--- +"@ylfjuk-core/utils": patch +"@ylfjuk-core/types": patch +"@ylfjuk/core": patch +--- + +BREAKING CHANGE: remove `Enum.toTuple`, its `enumToTuple` alias, and the otherwise empty `Enum` namespace from `@ylfjuk-core/utils` and the `@ylfjuk/core` umbrella re-exports. 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). + +Keep the public `UnionToTuple` type and clarify that its order is unspecified and unstable. It cannot predict `Object.values` order or recover duplicate values discarded by a union. + +These patch bumps follow this repository's existing pre-1.0 practice for breaking changes; they do not declare a 1.0 release. diff --git a/packages/core/README.md b/packages/core/README.md index 4fdb642..cd30ceb 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -16,6 +16,10 @@ import { isDefined } from "@ylfjuk/core"; It re-exports [`@ylfjuk-core/types`](../types/README.md) and [`@ylfjuk-core/utils`](../utils/README.md). +## Migration + +BREAKING CHANGE: the `Enum` namespace, `Enum.toTuple`, and `enumToTuple` re-exports have been removed. Use `Object.values` for an array, or define an explicit tuple when positions matter. See the [migration guide](../utils/README.md#migrate-from-enumtotuple) for examples and the ordering and duplicate-value limitations. `UnionToTuple` remains public with unspecified, unstable element order. + ## 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/types/src/union-to-tuple.ts b/packages/types/src/union-to-tuple.ts index 79b0df0..91aa0d6 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 + * import type { UnionToTuple } from "@ylfjuk-core/types"; + * * type Enum = "bert" | "berta" | "bob"; * - * type EnumAsTuple = UnionToTuple; // ["bert", "berta", "bob"] - * type IsEnumAsTuple = IsTuple; // true + * type EnumAsTuple = UnionToTuple; // Element order is unspecified. + * type Members = EnumAsTuple[number]; // "bert" | "berta" | "bob" + * type Length = EnumAsTuple["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"; From d69f6940c346e6b6f1c9e4f78834bf46402d1f89 Mon Sep 17 00:00:00 2001 From: YLfjuk Date: Sun, 18 Oct 2026 20:52:18 +0300 Subject: [PATCH 2/3] docs: separate CORE-04 release notes by package --- .changeset/clarify-union-to-tuple-order.md | 5 +++++ .changeset/remove-enum-to-tuple.md | 5 ----- packages/types/src/union-to-tuple.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 .changeset/clarify-union-to-tuple-order.md 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 index 6128209..6d5ca67 100644 --- a/.changeset/remove-enum-to-tuple.md +++ b/.changeset/remove-enum-to-tuple.md @@ -1,6 +1,5 @@ --- "@ylfjuk-core/utils": patch -"@ylfjuk-core/types": patch "@ylfjuk/core": patch --- @@ -9,7 +8,3 @@ BREAKING CHANGE: remove `Enum.toTuple`, its `enumToTuple` alias, and the otherwi 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). - -Keep the public `UnionToTuple` type and clarify that its order is unspecified and unstable. It cannot predict `Object.values` order or recover duplicate values discarded by a union. - -These patch bumps follow this repository's existing pre-1.0 practice for breaking changes; they do not declare a 1.0 release. diff --git a/packages/types/src/union-to-tuple.ts b/packages/types/src/union-to-tuple.ts index 91aa0d6..858530e 100644 --- a/packages/types/src/union-to-tuple.ts +++ b/packages/types/src/union-to-tuple.ts @@ -12,11 +12,11 @@ import type { LastOfUnion } from "./last-of-union.js"; * @example * import type { UnionToTuple } from "@ylfjuk-core/types"; * - * type Enum = "bert" | "berta" | "bob"; + * type Name = "bert" | "berta" | "bob"; * - * type EnumAsTuple = UnionToTuple; // Element order is unspecified. - * type Members = EnumAsTuple[number]; // "bert" | "berta" | "bob" - * type Length = EnumAsTuple["length"]; // 3 + * type NameTuple = UnionToTuple; // Element order is unspecified. + * type Members = NameTuple[number]; // "bert" | "berta" | "bob" + * type Length = NameTuple["length"]; // 3 * * @since 0.0.18 */ From e1ce15e5570e5d3dc0623595f748c35b4ff973e3 Mon Sep 17 00:00:00 2001 From: YLfjuk Date: Tue, 20 Oct 2026 11:28:19 +0300 Subject: [PATCH 3/3] docs: apply CORE-04 human review feedback --- .changeset/remove-enum-to-tuple.md | 2 +- packages/core/README.md | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.changeset/remove-enum-to-tuple.md b/.changeset/remove-enum-to-tuple.md index 6d5ca67..0972538 100644 --- a/.changeset/remove-enum-to-tuple.md +++ b/.changeset/remove-enum-to-tuple.md @@ -3,7 +3,7 @@ "@ylfjuk/core": patch --- -BREAKING CHANGE: remove `Enum.toTuple`, its `enumToTuple` alias, and the otherwise empty `Enum` namespace from `@ylfjuk-core/utils` and the `@ylfjuk/core` umbrella re-exports. Imports of these APIs must be removed. +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. diff --git a/packages/core/README.md b/packages/core/README.md index cd30ceb..4fdb642 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -16,10 +16,6 @@ import { isDefined } from "@ylfjuk/core"; It re-exports [`@ylfjuk-core/types`](../types/README.md) and [`@ylfjuk-core/utils`](../utils/README.md). -## Migration - -BREAKING CHANGE: the `Enum` namespace, `Enum.toTuple`, and `enumToTuple` re-exports have been removed. Use `Object.values` for an array, or define an explicit tuple when positions matter. See the [migration guide](../utils/README.md#migrate-from-enumtotuple) for examples and the ordering and duplicate-value limitations. `UnionToTuple` remains public with unspecified, unstable element order. - ## 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`.