From dc2a8f04b22265c4b3544522a3f97137d2a34c21 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 07:59:14 +0000 Subject: [PATCH 1/2] feat: add typedKeys, typedEntries, and typedFromEntries The `Object` builtins with the cast a call site would otherwise write by hand, as plain exported functions: a missing import is then a compile error at the call site, where a global installed by a side-effect import fails only at runtime. Mirrors TaskRankr #443, including #442's narrowing to plain records. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01GKtR2Uc6Jx7WzoWjq49SmC --- CLAUDE.md | 1 + src/utils.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9625774..7c963cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,4 +22,5 @@ Conventions live outside this file, synced from https://github.com/bvandrc/bvand ## Repo conventions - **Package manager**: pnpm. `npm install` writes a competing `package-lock.json` that CI ignores. +- **Typed `Object` walks**: `typedKeys`, `typedEntries`, and `typedFromEntries`, exported from `src/utils.ts`, are ours. They are the builtins with the cast a call site would otherwise write by hand, so reach for one instead of asserting the result -- and leave `Object.keys`/`entries`/`fromEntries` alone where the widened type is what's wanted. - **Convention files**: `conventions/` is synced from https://github.com/bvandrc/bvandrc-conventions by `.github/workflows/sync-conventions.yml` and overwritten on every sync. Edit a rule upstream, never in that directory. diff --git a/src/utils.ts b/src/utils.ts index 31ab4ad..d58cc52 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,43 @@ +import type { Entries } from 'type-fest' + +/** + * `Object.keys`, typed as the keys of what was passed. + * + * Narrower than the runtime guarantees: a value can carry keys beyond the ones + * its type names, which is the cast being made on purpose. + * + * Restricted to plain records: an array's `keyof` names its string methods, not + * the numeric-as-string indices `Object.keys` actually returns, so an array + * argument is refused rather than mistyped. + */ +export const typedKeys = >(o: T) => + Object.keys(o) as (keyof T & string)[] + +/** + * `Object.entries`, typed as the entries of what was passed. + * + * Carries the key-to-value correlation that `Object.entries` drops, so + * destructuring an entry of a union-valued object narrows. Refuses an array + * argument for the same reason as `typedKeys`. + */ +export const typedEntries = >(o: T) => + Object.entries(o) as Entries + +/** + * `Object.fromEntries`, typed as the object those entries build. + * + * Only as precise as the entries it is handed, so a `.map` that wants each key + * paired with its own value type annotates the callback's return as a tuple. + */ +export const typedFromEntries = < + const EntryList extends readonly (readonly [PropertyKey, unknown])[], +>( + entries: EntryList +) => + Object.fromEntries(entries) as { + [Entry in EntryList[number] as Entry[0]]: Entry[1] + } + export type AnyFunctionComponent = React.FunctionComponentElement /** The component's name, used to title the suite written for it. */ From 90d4ab60f8b9cfea36d7b947d7392c0d6d1a5004 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 08:36:26 +0000 Subject: [PATCH 2/2] refactor: move src/utils.ts to src/utils/index.ts Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01GKtR2Uc6Jx7WzoWjq49SmC --- CLAUDE.md | 4 ++-- src/{utils.ts => utils/index.ts} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/{utils.ts => utils/index.ts} (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 7c963cb..4899fcf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ `react-component-test-suite` — consistent, reusable test suites for React components in Jest or Vitest, naming each `describe` block after the component under test. Published to npm, bundled by tsdown. -- **Layout**: `src/component-suite.tsx` is the entrypoint, `src/utils.ts` its helpers; unit tests live in `src/__tests__/` with shared fixtures in `src/__tests__/__helpers__/`. +- **Layout**: `src/component-suite.tsx` is the entrypoint, `src/utils/index.ts` its helpers; unit tests live in `src/__tests__/` with shared fixtures in `src/__tests__/__helpers__/`. - **Peers**: `react`, `@types/react`, and the Testing Library packages are peers; `vitest` and `jest` are optional peers, so nothing may import either test runner at module scope. ## Code conventions @@ -22,5 +22,5 @@ Conventions live outside this file, synced from https://github.com/bvandrc/bvand ## Repo conventions - **Package manager**: pnpm. `npm install` writes a competing `package-lock.json` that CI ignores. -- **Typed `Object` walks**: `typedKeys`, `typedEntries`, and `typedFromEntries`, exported from `src/utils.ts`, are ours. They are the builtins with the cast a call site would otherwise write by hand, so reach for one instead of asserting the result -- and leave `Object.keys`/`entries`/`fromEntries` alone where the widened type is what's wanted. +- **Typed `Object` walks**: `typedKeys`, `typedEntries`, and `typedFromEntries`, exported from `src/utils/index.ts`, are ours. They are the builtins with the cast a call site would otherwise write by hand, so reach for one instead of asserting the result -- and leave `Object.keys`/`entries`/`fromEntries` alone where the widened type is what's wanted. - **Convention files**: `conventions/` is synced from https://github.com/bvandrc/bvandrc-conventions by `.github/workflows/sync-conventions.yml` and overwritten on every sync. Edit a rule upstream, never in that directory. diff --git a/src/utils.ts b/src/utils/index.ts similarity index 100% rename from src/utils.ts rename to src/utils/index.ts