Skip to content

Keep the ListResult wrapper on paginated arrays whose entity has no alias, and typecheck the tests - #754

Merged
jeremy merged 5 commits into
mainfrom
fix/ts-listresult-fallback
Aug 17, 2026
Merged

Keep the ListResult wrapper on paginated arrays whose entity has no alias, and typecheck the tests#754
jeremy merged 5 commits into
mainfrom
fix/ts-listresult-fallback

Conversation

@jeremy

@jeremy jeremy commented Aug 17, 2026

Copy link
Copy Markdown
Member

Closes #737.

Four generated TypeScript methods declared a bare *ResponseContent array where the runtime returns a ListResultlistGauges, listGaugeNeedles, search, reminders — so result.meta.totalCount was a type error on four methods whose own JSDoc promises it. Two commits: turn on the instrument that should have caught it, then fix the generator.

The generator fix

buildReturnType() resolves element type names only through the hand-maintained TYPE_ALIASES map. Gauge, GaugeNeedle, SearchResult and QuestionReminder are not in it, so getEntityTypeName() returned null and the function fell through to its "fallback to schema ref" line, which dropped the ListResult<> wrapper entirely — even with op.returnsArray && op.hasPagination both true.

Adding the four missing aliases would fix four symptoms and leave the mechanism waiting for the next unaliased paginated entity. Instead the fallback is pagination-aware: a paginated array keeps its wrapper and only the element name degrades, to the item's own schema ref.

// before, for an entity with no alias
Promise<components["schemas"]["ListGaugesResponseContent"]>
// after
Promise<ListResult<components["schemas"]["Gauge"]>>

The spelling is ListResult<components["schemas"]["<Item>"]>, resolved from the response schema's items.$ref — the same components["schemas"][...] idiom the surrounding code already uses for unaliased refs, rather than an indexed access like …ResponseContent[number].

The second hole is covered too

Wrapped pagination (a list under a key of an object response) spelled an unaliased entity ListResult<unknown>, in two places that must agree: the declared return type and the requestPaginatedWrapped<key, T> type argument — each resolving the entity with its own copy of the expression. Both now call one buildPaginationElementType() helper that resolves the array form and the wrapped form identically, reaching unknown only for a schema that names no element at all. No generated output changes there today (the one wrapped operation's entity is aliased), but the two sites can no longer drift apart — and that is asserted rather than asserted-by-construction. Sharing a helper is not a guarantee; nothing observed the second site at all. Regressing its type argument to unknown and regenerating left make ts-check green end to end — 1525 tests, both typecheck projects, and the drift check, which passes once the output is regenerated to match. The generated method casts to its separately built return type, so the disagreement never reaches the compiler. generateMethod is now exported and the emitted method is asserted to name the same element as the declared return type, over the aliased hit and the unaliased miss; both cases fail against that mutation.

Generated diff: 4 lines across 3 files. make ts-check-drift (regenerate into a temp dir + diff) passes.

The blind spot, not just the bug

typescript/tsconfig.json sets "exclude": [..., "tests"], so npm run typecheck — all of make ts-typecheck, and a step in both test.yml and release-typescript.yml — never typechecked a single test file, and vitest strips types without checking them. A test asserting a type proves nothing while no job typechecks tests, so fixing the generator without this leaves the same bug free to come back.

tsconfig.json has to stay build-only (rootDir: "src", declaration emit), so this adds typescript/tsconfig.test.json, a typecheck-only project over src + tests + scripts, run from npm run typecheck after the build project. Two settings are load-bearing:

  • exclude is reset. extends inherits it, and an inherited "tests" entry silently drops every test file from the program even when include names them — the first draft of this config compiled zero test files and reported success. --listFiles | grep -c /tests/ said 0.
  • noUncheckedIndexedAccess is off. The shipped surface keeps it (the build project still checks src/ with it); in test code it only buys calls[0]! ceremony, since an undefined index fails the next assertion anyway. It accounts for 152 of the 269 errors.
  • lib is ES2023, matching what these files actually run on (tsx on Node >= 22.12, per engines); the shipped surface keeps ES2022.

Pre-existing errors this surfaced: 269 → 117 after the flag above, all fixed, none suppressed

No @ts-ignore, no @ts-expect-error, no blanket as any. Five classes:

class count fix
CFA narrowing to neverlet captured: T | null = null assigned only inside an MSW handler closure 37 hold it in an object so CFA can't narrow; absence still fails the test
TS1543 JSON fixture imports under module: NodeNext 16 with { type: "json" } (vite honors it; suite green)
vitest Mock<Procedure | Constructable> vs Pick<Console, …> 14 give vi.fn() its signature type argument
optional schema fields read without a presence assertion 8 assert toBeDefined() first, so a missing field fails with a clear message
a Record<string, unknown[]> cast in my-notifications 4 removed — it erased the typing of the very fields under test

One real finding. Three tests drove paths that do not exist in the spec at all: /todolists/{todolistId}.json, /buckets/{projectId}/todolists/{todolistId}.json, and a PUT to /buckets/{bucketId}/todos/{todoId}.json. openapi-fetch substitutes path strings blindly and the MSW stubs were written to match the fabricated URL, so both sides agreed and the tests proved client behavior at a URL the SDK will never emit. They now use the modelled paths (/todolists/{id}, /todos/{todoId}) with stubs corrected to what the client actually sends; every assertion is unchanged. Three as never casts on client.PUT bodies went with them — against a real path the bodies typecheck.

scripts/generate-services.ts joins the program via the generator test that imports it; its serviceName assignment is now a conditional expression rather than a let filled in from inside a loop. Regenerating with that commit's generator reproduces the committed tree byte for byte.

Red proofs

The bug's own test, against the un-fixed generator — full gate wired, generator at main's behavior, regenerated output identical to main:

tests/types/paginated-returns.test-d.ts(53,45): error TS2344: Type 'false' does not satisfy the constraint 'true'.
… 8 errors: IsListResult + HasListMeta for all four methods
REAL_EXIT=2

The element-type, wrapped-pagination, TodosService#list and negative-control assertions all passed in that run — i.e. the failures are exactly the missing wrapper, not a blanket break.

Per-case mutation, wrapped form: forcing buildPaginationElementType back to unknown at the two wrapped sites (verified 2 sites matched, lines printed, checked on disk) and regenerating fails exactly one assertion — PersonProgressElement, line 89 — REAL_EXIT=2. Restored by cp + diff -q, regenerated, tree back to the 4-line diff, REAL_EXIT=0.

Vacuity guard: SingleGaugeNeedleIsNotListResult asserts a single-entity method does not satisfy the predicate, so the four positives can't pass because the predicate always says true.

Verification

make ts-check (drift + typecheck + tests) and make doc-constants-check both REAL_EXIT=0 locally, exit codes read back from log files. Per-repo convention I'm not quoting local vitest counts — CI is the authority.


Summary by cubic

Keeps ListResult<T> in generated return types for paginated arrays even when the entity has no alias, adds a typecheck-only project for tests and scripts, and locks wrapped-pagination signatures so the declared element and the requestPaginatedWrapped<key, T> argument cannot drift. No runtime change.

  • Generator: buildReturnType preserves ListResult on array pagination and uses buildPaginationElementType(op) for both declared types and requestPaginatedWrapped<key, T>. Service routing is now a single conditional expression. Exports buildReturnType, generateMethod, and ParsedOperation for tests.

  • Updated signatures: GaugesService#listGauges, GaugesService#listGaugeNeedles, SearchService#search, and CheckinsService#reminders return Promise<ListResult<components["schemas"]["..."]>> with typed .meta.

  • Typecheck and tests: adds tsconfig.test.json and runs it via npm run typecheck; adds generator/unit assertions that the wrapped element matches in both places; fixes test typings (JSON import attributes, MSW captures, typed vi.fn, presence checks) and corrects a few test paths to modeled endpoints.

  • Review notes

    • Confirm both the declared return type and requestPaginatedWrapped use the same element via buildPaginationElementType(op).
    • Verify the four methods expose .meta and that npm run typecheck covers src plus tests/scripts. No migration required.

Written for commit 6908bb6. Summary will update on new commits.

Review in cubic

Copilot AI balanced review requested due to automatic review settings August 17, 2026 05:04
@github-actions github-actions Bot added the typescript Pull requests that update TypeScript code label Aug 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes TypeScript pagination return types and expands typechecking to tests and scripts.

Changes:

  • Preserves ListResult<T> for unaliased paginated entities.
  • Adds a test-focused TypeScript configuration and type assertions.
  • Resolves surfaced test typing issues and invalid endpoint paths.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 29 out of 32 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
typescript/tsconfig.test.json Adds test and script typechecking.
typescript/package.json Runs both TypeScript projects.
typescript/README.md Documents expanded typechecking.
typescript/scripts/generate-services.ts Preserves concrete paginated return types.
typescript/src/generated/services/checkins.ts Corrects reminders return type.
typescript/src/generated/services/gauges.ts Corrects gauge list return types.
typescript/src/generated/services/search.ts Corrects search return type.
typescript/tests/types/paginated-returns.test-d.ts Adds pagination type assertions.
typescript/tests/auth-strategy.test.ts Makes request captures type-safe.
typescript/tests/client.test.ts Fixes captures and todolist path.
typescript/tests/hooks.test.ts Types console mocks.
typescript/tests/integration.test.ts Uses the modeled todolist path.
typescript/tests/middleware-lifecycle.test.ts Uses the modeled todo path.
typescript/tests/security.test.ts Adds a typed page parser.
typescript/tests/services/boosts.test.ts Narrows optional booster data.
typescript/tests/services/cards.test.ts Adds JSON import attributes.
typescript/tests/services/client-visibility.test.ts Adds JSON import attributes.
typescript/tests/services/comments.test.ts Adds JSON import attributes.
typescript/tests/services/documents.test.ts Fixes response and request typing.
typescript/tests/services/gauges.test.ts Tests typed pagination metadata directly.
typescript/tests/services/hill-charts.test.ts Narrows optional dot collections.
typescript/tests/services/messages.test.ts Adds a JSON import attribute.
typescript/tests/services/my-notifications.test.ts Removes response-erasing casts.
typescript/tests/services/recordings.test.ts Makes URL captures type-safe.
typescript/tests/services/schedules.test.ts Fixes response and request typing.
typescript/tests/services/search.test.ts Adds a JSON import attribute.
typescript/tests/services/subscriptions.test.ts Narrows optional subscribers.
typescript/tests/services/todolists.test.ts Adds JSON import attributes.
typescript/tests/services/todos.test.ts Uses MSW’s JSON body type.
typescript/tests/services/tools.test.ts Adds JSON import attributes.
typescript/tests/services/uploads.test.ts Improves request and rejection typing.
typescript/tests/services/vaults.test.ts Makes body captures type-safe.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread typescript/tests/types/paginated-returns.test-d.ts
Comment thread typescript/scripts/generate-services.ts
Copilot AI review requested due to automatic review settings August 17, 2026 05:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 33 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef8e3ef0ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread typescript/tsconfig.test.json
Copilot AI review requested due to automatic review settings August 17, 2026 05:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 33 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings August 17, 2026 05:49
@jeremy
jeremy force-pushed the fix/ts-listresult-fallback branch from d8c84d6 to 4d08c85 Compare August 17, 2026 05:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 33 changed files in this pull request and generated no new comments.

@jeremy

jeremy commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 4d08c85f26

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

jeremy added 4 commits August 17, 2026 10:50
`typescript/tsconfig.json` is the build project: it emits declarations under
`rootDir: "src"`, and it excludes `tests`. `npm run typecheck` -- the whole of
`make ts-typecheck`, and a step in both the test and release workflows -- ran
only that project, so no job had ever typechecked a single test file. vitest
strips types without checking them, so the test tree was unchecked from both
sides.

That is not a hypothetical gap. It is what let issue #737 ship: four generated
methods declared a return type the runtime contradicts, and a test that reads
`.meta` on one of them compiles clean here either way. It is also why no
`TS1543` import-attribute diagnostic has ever surfaced.

Add `tsconfig.test.json`, a typecheck-only project over src + tests + scripts,
and run it from `npm run typecheck` after the build project. Two of its
settings are load-bearing and easy to get wrong:

  - `exclude` is reset. `extends` inherits it, and an inherited "tests" entry
    silently drops every test file from the program even when `include` names
    them -- the program compiles, reports nothing, and proves nothing.
  - `noUncheckedIndexedAccess` is off. The shipped surface keeps it (the build
    project still checks src/ with it); in test code it only buys `calls[0]!`
    ceremony, since an undefined index fails the next assertion anyway.

Turning it on surfaced 269 pre-existing errors, 117 of them after the flag
above. They fall into five classes, all fixed here rather than suppressed --
no `@ts-ignore`, no `@ts-expect-error`, no blanket `as any`:

  - CFA narrowing to `never`: a `let captured: T | null = null` assigned only
    inside an MSW handler closure narrows to `null` at the assertions. Held in
    an object instead, so absence still fails the test.
  - `TS1543`: JSON fixture imports need `with { type: "json" }` under
    `module: NodeNext`. vite honors the attribute; the suite is green with it.
  - vitest `Mock<Procedure | Constructable>` against `Pick<Console, ...>`:
    give `vi.fn()` its signature type argument.
  - optional schema fields read without a presence assertion: assert first, so
    a missing field fails with a clear message.
  - a `Record<string, unknown[]>` cast in my-notifications that erased the
    typing of the very fields under test.

Three tests were driving paths that do not exist in the spec at all --
`/todolists/{todolistId}.json`, `/buckets/{projectId}/todolists/{todolistId}.json`,
and a PUT to `/buckets/{bucketId}/todos/{todoId}.json`. openapi-fetch
substitutes path strings blindly and the MSW stubs were written to match the
fabricated URL, so both sides agreed and the tests proved client behavior at a
URL the SDK will never emit. They now use the modelled paths (`/todolists/{id}`,
`/todos/{todoId}`) with the stubs corrected to what the client actually sends;
every assertion is unchanged. Three `as never` casts on `client.PUT` bodies
went with them -- against a real path the bodies typecheck.

`scripts/generate-services.ts` joins the program via the generator test that
imports it. Its `serviceName` assignment is now a conditional expression rather
than a `let` filled in from inside a loop, which is what TS could not prove
definite. Output is unchanged: regenerating with this commit's generator
reproduces the committed `src/generated/` tree byte for byte, and
`make ts-check-drift` passes.
`buildReturnType()` resolved element type names only through the
hand-maintained `TYPE_ALIASES` map. `Gauge`, `GaugeNeedle`, `SearchResult` and
`QuestionReminder` are not in it, so `getEntityTypeName()` returned null and
the function fell through to its "fallback to schema ref" line -- which
returned the bare `*ResponseContent` array and dropped `ListResult<>`
entirely, even with `op.returnsArray && op.hasPagination` both true.

The runtime object is a `ListResult` in all four cases: `requestPaginated`
builds one, and the JSDoc on all four already promises ".meta.totalCount". So
the docs and the runtime agreed with each other and only the signature
disagreed, making `result.meta.totalCount` a type error on four methods where
every sibling allows it. Fixes #737.

Adding the four missing aliases would have fixed four symptoms and left the
mechanism intact, waiting for the next unaliased paginated entity. Instead the
fallback is now pagination-aware: a paginated array keeps its wrapper and only
the element name degrades, to the item's own schema ref -- which is a perfectly
good element type, just not a friendly one.

The same lookup had a second hole one level down. Wrapped pagination (a list
under a key of an object response) spelled an unaliased entity
`ListResult<unknown>`, in two places that have to agree: the declared return
type and the `requestPaginatedWrapped<key, T>` type argument, each resolving
the entity with its own copy of the expression. Both now call one
`buildPaginationElementType()` helper, which resolves the array form and the
wrapped form the same way and reaches `unknown` only for a schema that names
no element at all. No generated output changes there today -- the one wrapped
operation's entity is aliased -- but the two sites can no longer drift apart.

`tests/types/paginated-returns.test-d.ts` pins the contract in the type system,
where the previous commit's typecheck can see it: each of the four methods
returns a `ListResult` carrying `.meta`, with a concrete element type. It
covers the wrapped shape and the already-aliased `TodosService#list` too, and a
negative control asserts a single-entity method does NOT satisfy the predicate,
so the assertions cannot pass vacuously. Reverting this commit's generator
change and regenerating fails it with 8 TS2344s; forcing the wrapped element
back to `unknown` fails exactly the one assertion that covers it.

`tests/services/gauges.test.ts` drops the `metaOf()` helper that asserted
`instanceof ListResult` at runtime to reach `.meta` past the wrong signature.
It now reads `gauges.meta` directly, with one explicit `toBeInstanceOf`
assertion left in the first test to pin the runtime class behind the type.
…reach

The type-level assertions pin the four real operations that hit the array-form
alias miss. They cannot reach the wrapped form: the one wrapped-pagination
operation in the spec carries an aliased entity, so no generated signature
would move if that branch regressed to `unknown` again. Raised in review.

Drive `buildReturnType` directly instead, following the generator-regression
pattern `tests/generator/example-value.test.ts` already establishes
(`setSchemas` + an exported function). Six cases: aliased and unaliased for
both the array and the wrapped form, the unpaginated array that must still
fall back to its schema ref, and the floor where the items name no schema at
all and only `ListResult<unknown>` is left.

The unaliased entity is a fictional `WidgetThing` rather than a real one, so a
later TYPE_ALIASES addition cannot quietly turn a miss case into a hit case.

Reverting the array fallback fails 2 of the 6; restoring the wrapped
alias-miss to `unknown` fails exactly 1 -- the case that has no other cover.
Two review bots independently read `paginated-returns.test-d.ts` as a
declaration file and concluded the assertions are inert under the inherited
`skipLibCheck: true`. It ends in `-d.ts`, not `.d.ts`, so TypeScript checks it
like any other source -- flipping an assertion to a knowingly false one
reports TS2344 on that line, and the branch's red proof is 8 such errors in
that file.

Their proposed remedy is worse than the disease: `skipLibCheck: false` fails
on 22 errors inside @mswjs/interceptors' browser `.d.mts` and adds ~20s per
run. Write both facts where the next reader looks -- the file header and the
config -- with the recipe to re-verify in one command, so the next reader
checks instead of re-opening.
Copilot AI review requested due to automatic review settings August 17, 2026 17:52
@jeremy
jeremy force-pushed the fix/ts-listresult-fallback branch from 4d08c85 to 3a7a508 Compare August 17, 2026 17:52
@jeremy

jeremy commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 3a7a508275

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 33 changed files in this pull request and generated no new comments.

Suppressed comments (1)

typescript/tests/generator/pagination-return-type.test.ts:160

  • This regression case only calls buildReturnType, so it verifies the declared wrapper but not the second changed call site in generateMethod. Replacing the new requestPaginatedWrapped<..., buildPaginationElementType(op)> argument with unknown would leave this suite and paginated-returns.test-d.ts green: the current wrapped operation is aliased, and the generated method casts to the separately built return type. Exercise the emitted wrapped method and assert that its generic argument is components["schemas"]["WidgetThing"] too, so the two sites are actually locked together.
    it("uses the item's schema ref when the entity has no alias", () => {
      const returnType = buildReturnType(
        operation({
          responseSchemaRef: "WidgetReportResponseContent",
          returnsArray: false,
          hasPagination: true,
          paginationKey: "widgets",
        }),
        "Reports",
      );

      expect(returnType).toBe('{ person: Person; widgets: ListResult<components["schemas"]["WidgetThing"]> }');

@jeremy

jeremy commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main at 53d52d2ea; new head 3a7a50827. The rebase was the point, not housekeeping: the previous head's green was earned against a main that did not yet contain #752's check-service-inventory-parity gate, so it proved nothing about it.

That gate reads the TypeScript generated-services directory. This branch modifies exactly three files there (checkins.ts, gauges.ts, search.ts) and adds or removes none, so the rendering should be unchanged — verified rather than assumed, against the rebased tree:

  • make check-service-inventory-parity — passes
  • make test-check-service-inventory-parity — 15 cases plus the positive control, all pass
  • make doc-constants-check — 27 marked spans across 7 files, all match
  • make lint-npm-lockfile-writes — passes
  • make ts-check — drift, typecheck (both projects) and the suite, all pass

Three dependabot bumps came in with the rebase, one of them the npm-dependencies group touching typescript/package.json, which this branch also touches; the rebase applied clean with no conflict. Test counts come from the CI job rather than this run — a local vitest and CI have disagreed on identical input before.

The claim that the declared return type and the `requestPaginatedWrapped`
type argument can no longer drift apart was unenforced: both call the same
`buildPaginationElementType`, but only the first is asserted anywhere.

Regressing the second to `unknown` and regenerating leaves `make ts-check`
entirely green — 1525 tests, both typecheck projects, and the drift check,
which passes because the committed output was regenerated to match. The
generated method casts its result to the separately built return type, so
the disagreement never reaches the compiler; `paginated-returns.test-d.ts`
cannot reach it either, since it pins the four operations that hit the
array miss.

Export `generateMethod` and assert the emitted method names the same
element as the declared return type, for the aliased hit and the unaliased
miss. Both cases fail against that mutation.
Copilot AI review requested due to automatic review settings August 17, 2026 18:04
@jeremy

jeremy commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Answering the suppressed finding on the newest review — pagination-return-type.test.ts:160, that the regression case only calls buildReturnType and so leaves the second changed call site unlocked.

Confirmed, and it is worse than stated. Fixed in 6908bb613.

I mutated generateMethod's requestPaginatedWrapped<"${op.paginationKey}", ${buildPaginationElementType(op)}> to a literal unknown and regenerated, so the committed output matched the mutated generator:

mutated generator, output regenerated
pagination-return-type.test.ts passes
paginated-returns.test-d.ts passes
make ts-typecheck (both projects) passes
make ts-check-drift passes
make ts-check — 85 files, 1525 tests passes, exit 0

Generated reports.ts reads requestPaginatedWrapped<"events", unknown> throughout that run. So nothing in the repo observes the regression — not just the two suites named. Drift catches the generator edit alone, but a real change edits the generator and regenerates, which is the sequence above.

The mechanism is the one you identified: the generated method casts its result to the separately built return type, so the two never meet at the compiler. And the type assertions cannot reach it, since the single wrapped-pagination operation in the spec carries an aliased entity — the same reason the unit case exists in the first place.

That makes the PR's claim that the two sites "can no longer drift apart" an unenforced one. Enforced now rather than restated: generateMethod is exported, and the emitted method is asserted to name the same element as the declared return type, it.each-driven over both branches —

  • aliased hit: requestPaginatedWrapped<"events", TimelineEvent> and events: ListResult<TimelineEvent>
  • unaliased miss: requestPaginatedWrapped<"widgets", components["schemas"]["WidgetThing"]> and the matching declared element

Both cases fail against the unknown mutation — per case, not one covering for the other — and both pass with it restored. make ts-check is green at 6908bb613: 85 files, 1527 tests, no drift. Generator restored by cp and verified with diff -q; the only other file the regeneration touched was metadata.ts's generated timestamp, diffed and reverted.

Thanks — this was a real hole, and the suppressed block was the only place it surfaced.

@jeremy

jeremy commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 33 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 6908bb613d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jeremy
jeremy merged commit 13caea3 into main Aug 17, 2026
45 checks passed
@jeremy
jeremy deleted the fix/ts-listresult-fallback branch August 17, 2026 18:10
@jeremy jeremy added the breaking Breaking change to public API label Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaking change to public API typescript Pull requests that update TypeScript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TypeScript: four paginated methods declare a bare array return, hiding .meta from the compiler

2 participants