Summary
A computed deriver cannot reference a static on the entity being declared. The options object is evaluated inside the extends clause, so the class binding does not exist yet and TypeScript reports a circular base expression. The effect is that derivation logic must live outside the class, even when it is unambiguously that entity's own business.
Versions: @btravstack/entity@0.7.0, zod@4.3.6, typescript@5.9.3, --strict --module nodenext.
Reproduction
import { Entity } from "@btravstack/entity";
import { z } from "zod";
const Id = z.string().brand("Id");
const Tag = z.enum(["ARCHIVED", "PRIORITY"]);
const SUPPRESSING: readonly string[] = ["ARCHIVED"];
abstract class ViaOwnStatic extends Entity.abstract("A")(
{ id: Entity.field(Id, { generated: true }), tags: z.array(Tag) },
{
computed: {
active: Entity.computed(z.boolean(), (d) => ViaOwnStatic.isActive(d.tags)),
},
},
) {
static isActive(tags: readonly string[]): boolean {
return !tags.some((t) => SUPPRESSING.includes(t));
}
}
error TS2506: 'ViaOwnStatic' is referenced directly or indirectly in its own base expression.
error TS7024: Function implicitly has return type 'any' because it does not have a return
type annotation and is referenced directly or indirectly in one of its
return expressions.
The failure also cascades: once the base expression is poisoned, this.<field>, this.update and .extend stop resolving on that class, so the error surface is much larger than the one line that caused it.
A static on a separate class is fine:
class Policy {
static isActive(tags: readonly string[]): boolean {
return !tags.some((t) => SUPPRESSING.includes(t));
}
}
abstract class ViaOtherClass extends Entity.abstract("B")(
{ id: Entity.field(Id, { generated: true }), tags: z.array(Tag) },
{ computed: { active: Entity.computed(z.boolean(), (d) => Policy.isActive(d.tags)) } },
) {}
so this is purely about self-reference from inside the extends clause, not about what a deriver is allowed to do.
To be clear about what is not being asked
A deriver taking this would be wrong, and the current signature is right to exclude it. Derivation runs during construction, before an instance exists, and restricting from to the declared fields is what stops a computed value depending on another computed value. That constraint is well documented and I am not asking for it to change.
The ask is narrower: a deriver is a pure function of declared data, and there is no semantic reason it cannot be the entity's own pure function.
Why it matters
Reviewers reasonably expect an entity's derivation rules to sit on the entity. We were asked in review to move exactly this kind of logic onto the class, and had to answer that the library does not allow it and explain TS2506 — which is a language-level symptom of a declaration shape, not something a reader can be expected to infer.
The workaround (a module-level function beside the class) is perfectly serviceable, and for a small pure helper it is arguably fine. It just leaves a visible seam: every other rule on the entity is a method, and the derivation rules are not, for a reason that has nothing to do with the domain.
Possible directions
I do not know which of these is viable inside the type machinery, so treat them as prompts rather than a proposal:
- A second deriver argument carrying the entity's statics, resolved at construction time when the class does exist —
Entity.computed(schema, (d, self) => self.isActive(d.tags)). May well be circular in the type even if fine at runtime.
- Declaring
computed in the class body rather than the options object, so the binding exists — though the derived schemas are needed to build output / updateInput, so this may be incompatible with when those types are formed.
- Documenting it under
computed, with the separate-class and module-function workarounds named. Cheapest, and would have saved us the round trip: the current docs explain why d is the declared fields, but not that the deriver cannot be the entity's own static.
Even (3) alone would close this from my side. Happy to test a candidate.
Summary
A
computedderiver cannot reference astaticon the entity being declared. The options object is evaluated inside theextendsclause, so the class binding does not exist yet and TypeScript reports a circular base expression. The effect is that derivation logic must live outside the class, even when it is unambiguously that entity's own business.Versions:
@btravstack/entity@0.7.0,zod@4.3.6,typescript@5.9.3,--strict --module nodenext.Reproduction
The failure also cascades: once the base expression is poisoned,
this.<field>,this.updateand.extendstop resolving on that class, so the error surface is much larger than the one line that caused it.A static on a separate class is fine:
so this is purely about self-reference from inside the
extendsclause, not about what a deriver is allowed to do.To be clear about what is not being asked
A deriver taking
thiswould be wrong, and the current signature is right to exclude it. Derivation runs during construction, before an instance exists, and restrictingfromto the declared fields is what stops a computed value depending on another computed value. That constraint is well documented and I am not asking for it to change.The ask is narrower: a deriver is a pure function of declared data, and there is no semantic reason it cannot be the entity's own pure function.
Why it matters
Reviewers reasonably expect an entity's derivation rules to sit on the entity. We were asked in review to move exactly this kind of logic onto the class, and had to answer that the library does not allow it and explain
TS2506— which is a language-level symptom of a declaration shape, not something a reader can be expected to infer.The workaround (a module-level function beside the class) is perfectly serviceable, and for a small pure helper it is arguably fine. It just leaves a visible seam: every other rule on the entity is a method, and the derivation rules are not, for a reason that has nothing to do with the domain.
Possible directions
I do not know which of these is viable inside the type machinery, so treat them as prompts rather than a proposal:
Entity.computed(schema, (d, self) => self.isActive(d.tags)). May well be circular in the type even if fine at runtime.computedin the class body rather than the options object, so the binding exists — though the derived schemas are needed to buildoutput/updateInput, so this may be incompatible with when those types are formed.computed, with the separate-class and module-function workarounds named. Cheapest, and would have saved us the round trip: the current docs explain whydis the declared fields, but not that the deriver cannot be the entity's own static.Even (3) alone would close this from my side. Happy to test a candidate.