Skip to content

docs: bless the annotated self-referencing deriver idiom - #65

Merged
btravers merged 5 commits into
mainfrom
fix/computed-self-reference
Aug 11, 2026
Merged

docs: bless the annotated self-referencing deriver idiom#65
btravers merged 5 commits into
mainfrom
fix/computed-self-reference

Conversation

@btravers

Copy link
Copy Markdown
Contributor

Closes#60.

A computed deriver or an Entity.invariant predicate can reference the entity's own statics. The missing piece was an explicit return-type annotation on the deriver, not a library change — so this branch documents and pins the idiom, and changes no API.

classDocextendsEntity("Doc")({id: Entity.field(Id,{immutable: true}),tags: z.array(Tag)},{computed: {active: Entity.computed(z.boolean(),(d): boolean=>Doc.isActive(d.tags)),},},){staticisActive(tags: Tags): boolean{ ... }}

What was measured

On TypeScript 7.0.2 (repo) and 5.9.3 (the typescript-consumer fixture version):

  • The self-reference is not what breaks — the deriver's inferred return type is. TypeScript resolves a context-sensitive arrow's return eagerly while checking the heritage-clause call, so the body's Doc.isActive needs the class type mid-resolution. The annotation preempts that inference; the body itself is checked later, once the class type is settled. Unannotated is TS2506 on the class plus TS7024 on the arrow, and it cascades — one cause produced seven errors, since make/input/output/createInput/updateInput all vanish with the poisoned base expression.
  • No library-side signature spelling rescues the unannotated form.NoInfer<z.input<T>>, an unknown return position, and a curried shape all still produce TS2506 on a minimal model. The annotation is the only lever and it lives at the call site.
  • The annotation is not an escape hatch. Both faces report as TS2322 on the deriver's return expression: a body disagreeing with the annotation, and an annotation disagreeing with the schema (or widened to unknown). d also stays contextually typed — an undeclared field is still an error.
  • this is a hard dead end. A this: typeof Doc parameter is signature position, never deferred, so it is circular even with the return annotated (TS2502) — and the library cannot supply that type either, because the statics live in a class body TypeScript has not yet formed.
  • A variant referencing its root's statics needs no annotation — the root is an already-settled class.

Changes

  • packages/entity/src/computed.test-d.ts (new) — pins the idiom on concrete entities, Entity.abstract roots and .extend variants; both wrong-annotation faces isolated one error each; d still typed; and a dead-end ledger for the unannotated deriver, the unannotated predicate, the unannotated root and variant paths, and the this parameter. Every @ts-expect-error was verified to be consumed at its exact placement, on both compilers.
  • packages/entity/src/computed.ts, invariant.ts — one JSDoc paragraph each.
  • docs/explanation/computed-fields.md — a ## Self-referencing derivers section quoting TS2506 verbatim so the error is searchable into the docs; docs/reference/declaration.md gets two pointers.
  • Patch changeset — only the JSDoc reaches users, inside the published .d.ts.

examples/billing-domain is deliberately untouched: the options object is erased from the emitted .d.ts, so a self-referencing computed there would guard nothing.

Gate

format --check, lint, typecheck (both workspaces, including the 7.0.2 + 5.9.3 double-compile and the .emit-check pass), test (207), knip, build — all green.

Two claims in the #60 documentation were wrong on remeasurement: both
faces of the annotation check report TS2322 on the deriver's return
expression (not one at the deriver body and one at the Entity.computed
call), and the WrongBody test-d pin disagreed on both faces at once,
making its single @ts-expect-error vacuous. Also adds negative pins for
the unannotated form on Entity.abstract roots and .extend variants
(base.ts is a different builder than entity.ts), clarifies the
changeset wording (Entity.invariant takes a predicate, not an
"invariants deriver"), and notes the change is documentation only.
CopilotAI lite review requested due to automatic review settings August 11, 2026 17:19

CopilotAI 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

Documents and type-checks the “self-referencing deriver” idiom: computed derivers and Entity.invariant predicates may call the declaring entity’s own statics if the function has an explicit return-type annotation, avoiding the TS2506/TS7024 circularity triggered by inferred return types in the heritage clause. This is positioned as a documentation + regression-guard change with no runtime/API behavior change (closes #60).

Changes:

  • Added a new .test-d.ts file that pins the working annotated pattern and the known dead ends (TS2506, TS7024, TS2502) across entity, root, and variant cases.
  • Expanded JSDoc in computed and invariant to describe the pattern and point to the regression test.
  • Updated docs (explanation + reference) and added a patch changeset to ship the JSDoc updates.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
packages/entity/src/computed.tsAdds JSDoc guidance describing the annotated self-static deriver idiom and why it avoids TS2506.
packages/entity/src/invariant.tsAdds parallel JSDoc guidance for invariant predicates calling the entity’s own statics.
packages/entity/src/computed.test-d.tsNew type-level regression guard covering the annotated success case, wrong-annotation failures, and dead-end patterns.
docs/explanation/computed-fields.mdAdds an explanation section documenting the pattern, error codes, and boundaries (root-static vs self-static, this dead end).
docs/reference/declaration.mdAdds brief reference-level pointers for both computed and invariant to the explanation section.
.changeset/computed-self-reference-annotation.mdPatch changeset documenting the user-facing JSDoc/doc update (no runtime/API changes).

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

@btravers
btravers merged commit 231f218 into mainAug 11, 2026
14 checks passed
@btravers
btravers deleted the fix/computed-self-reference branch August 11, 2026 17:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

computed derivers cannot reference the entity's own statics (TS2506)

2 participants

@btravers