Skip to content

feat: add an extend static for building a new entity from an existing one - #17

Merged
btravers merged 3 commits into
mainfrom
feat/entity-extend
Aug 7, 2026
Merged

feat: add an extend static for building a new entity from an existing one#17
btravers merged 3 commits into
mainfrom
feat/entity-extend

Conversation

@btravers

@btravers btravers commented Aug 7, 2026

Copy link
Copy Markdown
Contributor
class Person extends Entity("Person")({ id: PersonId, name: Name }) {}

class PersonWithAge extends Person.extend("PersonWithAge")({ age: Age }) {
  get isAdult(): boolean {
    return this.age >= 18;
  }
}

Why this is allowed when subclassing is not

#8 refused class Sub extends Person {}, and that stands. The two are
different things:

bare subclass extend
tag Person's its own
schemas Person's its own
equals identity indistinguishable from a Person distinct
fields identical a superset

A bare subclass is an alias you cannot tell apart from what it aliases.
extend is a fresh Entity(...) call, so the result is a genuine entity —
and, being a direct subclass of its own base, it passes the construction
guard unchanged. A bare subclass of an extension is still refused; there is a
test.

Options are inherited, merged per key, child winning

immutable, invariants and computed all carry over unless the child names
them. Dropping them would leave an extension quietly laxer than what it
extends, which is the opposite of what "extends" should mean. Each is pinned:
the parent's invariant still rejects, its immutable still shapes
updateInput, its computed field still re-derives on the child's update.

The limit worth knowing

extend rebuilds from the declaration — the field map and the options. A
getter written in the parent's class body is part of neither, so it does not
come along. Documented in the README and pinned by a test; re-declare it, or
put shared behaviour in a plain function.

I found this by asserting the opposite and having TypeScript disagree, which
was the more useful outcome.

Implementation

Each entity's declaration is recorded in a module-level WeakMap keyed by its
base class, rather than stored as a property — so nothing leaks onto the public
surface or into a consumer's emitted declarations, which #13 made a live
concern.

Gate

format --check, lint, typecheck (three passes), test (119, 11 files),
knip, build — all green.

Copilot AI lite review requested due to automatic review settings August 7, 2026 00:36

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

Adds an extend capability to the Entity(...)-generated class surface so consumers can build a new entity from an existing entity’s declaration (fields + options), instead of using forbidden “bare subclassing”. This fits the library’s design by keeping each extended type a fresh entity with its own tag/schemas/equals identity, while reusing the parent’s declared contract pieces.

Changes:

  • Introduces a new static extend(tag)(fields, options?) API on entity classes, implemented via a module-scoped WeakMap of declarations.
  • Updates public typings to include the extend method on EntityStatic.
  • Adds documentation + a dedicated test suite covering inheritance/override behavior and the “class-body members don’t carry” limit.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
README.md Documents the new extend API and clarifies how it differs from forbidden subclassing.
packages/entity/src/types.ts Adds the extend method to EntityStatic’s public type surface.
packages/entity/src/entity.ts Implements extend and records entity declarations in a module-level WeakMap.
packages/entity/src/extend.spec.ts Adds tests for field/schema inheritance, option inheritance/override, sealing, and the “no class-body members” limit.
.changeset/entity-extend.md Adds a changeset entry describing the new feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/entity/src/entity.ts
Comment thread packages/entity/src/types.ts Outdated
Comment thread packages/entity/src/types.ts
Comment thread .changeset/entity-extend.md Outdated
@btravers btravers changed the title feat: add Entity.extend for building a new entity from an existing one feat: add an extend static for building a new entity from an existing one Aug 7, 2026
@btravers
btravers merged commit eeed6cb into main Aug 7, 2026
13 checks passed
@btravers
btravers deleted the feat/entity-extend branch August 7, 2026 00:46
Sign up for free to 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.

2 participants