feat: add an extend static for building a new entity from an existing one - #17
Merged
Conversation
There was a problem hiding this comment.
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-scopedWeakMapof declarations. - Updates public typings to include the
extendmethod onEntityStatic. - 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this is allowed when subclassing is not
#8 refused
class Sub extends Person {}, and that stands. The two aredifferent things:
extendequalsidentityA bare subclass is an alias you cannot tell apart from what it aliases.
extendis a freshEntity(...)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,invariantsandcomputedall carry over unless the child namesthem. 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
immutablestill shapesupdateInput, its computed field still re-derives on the child'supdate.The limit worth knowing
extendrebuilds from the declaration — the field map and the options. Agetter 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
WeakMapkeyed by itsbase 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.