@btravstack/entity
@btravstack/entity
Namespaces
Interfaces
AggregateInstance
Defined in: types.ts:652
An aggregate instance: an entity's data, toJSON and sameIdentityAs, and emit in place of update. An interface, not a type alias, for emit's polymorphic this (TS2526 — see BaseInstance).
Type Parameters
| Type Parameter |
|---|
S extends Fields |
A extends Schemas |
Ev extends Events |
O extends string |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
sameIdentityAs | readonly | [{ [K in string | number | symbol]: S[K] extends { flags: { identity: true } } ? K : never }[keyof S]] extends [never] ? object : (other) => boolean | types.ts:659 |
Methods
emit()
emit(...events): Result<Decision<AggregateInstance<S, A, Ev, O>, output<Ev>>, never>;Defined in: types.ts:673
Folds events onto this state, verifies the result with make, and returns the decision. Events breaking an invariant, failing their own schema, or a throwing handler are defects: a decision that does not hold is a bug in the command, never something to persist.
A creation event (O, the keys of opens) is excluded: an aggregate that exists cannot be created again, so emitting one is a compile error, as passing a non-creation event to start is. The decision's events stay typed as the whole union, the type a repository or an outbox stores.
Parameters
| Parameter | Type |
|---|---|
...events | readonly Exclude<output<Ev>, { type: O; }>[] |
Returns
Result<Decision<AggregateInstance<S, A, Ev, O>, output<Ev>>, never>
toJSON()
toJSON(): DeepReadonly<OutputOf<S, A>>;Defined in: types.ts:658
Returns
DeepReadonly<OutputOf<S, A>>
BaseInstance
Defined in: types.ts:307
The instance-side shape every entity's Base class structurally has: the three prototype methods that survive Omit<Base, "update">, plus update itself. update is typed with polymorphic this — the same mechanism decode/make/create use on the static side — so org.update(...) yields the subclass (Organization, with its _tag and class-body members intact), not the structural BaseInstance shape. Expressed independently of any concrete Base class — see EntityStatic for why.
Type Parameters
| Type Parameter |
|---|
S extends Fields |
A extends Schemas |
Properties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
sameIdentityAs | readonly | [{ [K in string | number | symbol]: S[K] extends { flags: { identity: true } } ? K : never }[keyof S]] extends [never] ? object : (other) => boolean | Whether other is the same business entity: same identity scope (this class, or the abstract root that declared the identity), and every identity field equal by Object.is. Attributes are not compared. A property rather than a method so it can be absent — a compile error — on an entity that declares no identity. The not-callable arm is an inline literal, not a named alias, so TypeDoc has nothing undocumented to report; its property name is the diagnostic. | types.ts:318 |
Methods
toJSON()
toJSON(): DeepReadonly<OutputOf<S, A>>;Defined in: types.ts:308
Returns
DeepReadonly<OutputOf<S, A>>
update()
update(patch): Result<BaseInstance<S, A>, InvalidEntity>;Defined in: types.ts:321
Parameters
| Parameter | Type |
|---|---|
patch | PatchOf<S, A, ImmutableKeys<S>> |
Returns
Result<BaseInstance<S, A>, InvalidEntity>
ConstructionKey
Defined in: types.ts:283
A type-level construction lock: no outside code can produce a value assignable to Sealed<D>, so new SomeEntity(...) does not compile. It closes the constructor without a runtime check, which unthrown/no-throw forbids.
ConstructionKey is exported but unconstructable — a private constructor and a private field make it unforgeable structurally, and it has no runtime existence at all. Exporting it is what lets a consumer compile:
A declare const CtorKey: unique symbol kept module-private was measured to break every downstream library that emits declarations — TS4020: 'extends' clause of exported class 'Organization' has or is using private name 'CtorKey' — because a unique symbol in computed-key position cannot be named across a module boundary even when exported. An ordinary named property whose type is an exported class can, so the emitted .d.ts references it as import("@btravstack/entity").Sealed<…>.
A literal protected constructor was measured and does not work either: TypeScript refuses to assign a protected-constructor class to any construct signature (TS2684), so the statics could only return the base class rather than the subclass. private is worse still — TS2675, the declaration form class X extends Entity("X")(...) stops compiling outright.
DecisionKey
Defined in: types.ts:620
The unforgeable half of a Decision — the same construction as ConstructionKey, for the same reason: a private member makes a type no object literal can satisfy, and it emits as an ordinary declared class.
Type Aliases
AbstractEntity()
type AbstractEntity<Name, S, A> = RootInstance<S, A>;Defined in: types.ts:447
What Entity.abstract(name)(fields, options?) returns.
Deliberately not an entity: no make, no factory, no schema members. The absence of a tag is what lets extend intersect the receiver's instance type unmapped — see RootInstance. name labels the root in its defect message and never reaches an instance.
Type Parameters
| Type Parameter |
|---|
Name extends string |
S extends Fields |
A extends Schemas |
type new AbstractEntity(d): RootInstance<S, A>;What Entity.abstract(name)(fields, options?) returns.
Deliberately not an entity: no make, no factory, no schema members. The absence of a tag is what lets extend intersect the receiver's instance type unmapped — see RootInstance. name labels the root in its defect message and never reaches an instance.
Parameters
| Parameter | Type |
|---|---|
d | Sealed<OutputOf<S, A>> |
Returns
RootInstance<S, A>
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
entityName | readonly | Name | types.ts:449 |
Methods
extend()
extend<This, Tag2>(this, tag): <S2, A2>(fields, options?) => EntityStatic<Tag2, MergedFields<S, S2>, MergedComputed<A, A2>, BehaviourOf<This>>;Defined in: types.ts:461
A new entity carrying this root's fields plus more, under its own tag, and inheriting the instance half of the class body of whatever it was called on: its methods and accessors, but not its statics and not its field initialisers. extend rewires the instance prototype and nothing else, so a root's constructor never runs — see docs/reference/declaration.md.
The this parameter is what picks up a behaviour-only intermediate root: abstract class Auditable extends AccountBase { … } then Auditable.extend(...) carries both bodies.
Type Parameters
| Type Parameter |
|---|
This |
Tag2 extends string |
Parameters
| Parameter | Type |
|---|---|
this | This |
tag | Tag2 |
Returns
<S2, A2>(fields, options?) => EntityStatic<Tag2, MergedFields<S, S2>, MergedComputed<A, A2>, BehaviourOf<This>>
AggregateStatic()
type AggregateStatic<Tag, S, A, Ev, O> = ConstructedAggregate<Tag, S, A, Ev, O>;Defined in: types.ts:697
What Entity.aggregate(tag)(fields, options) returns. Deliberately not an EntityStatic: no update, no factories, no createInput/updateInput — state changes only through events, and start is creation. No _zod slot in the type either, so an aggregate cannot be nested as another entity's field: a root is referenced by id, never embedded.
O is the opening event types — a literal union of event names, so it costs a few characters in a consumer's declarations, unlike the field-key unions the dead-end ledger in GeneratedKeys warns about.
Type Parameters
| Type Parameter |
|---|
Tag extends string |
S extends Fields |
A extends Schemas |
Ev extends Events |
O extends string |
type new AggregateStatic(d): ConstructedAggregate<Tag, S, A, Ev, O>;What Entity.aggregate(tag)(fields, options) returns. Deliberately not an EntityStatic: no update, no factories, no createInput/updateInput — state changes only through events, and start is creation. No _zod slot in the type either, so an aggregate cannot be nested as another entity's field: a root is referenced by id, never embedded.
O is the opening event types — a literal union of event names, so it costs a few characters in a consumer's declarations, unlike the field-key unions the dead-end ledger in GeneratedKeys warns about.
Parameters
| Parameter | Type |
|---|---|
d | Sealed<OutputOf<S, A>> |
Returns
ConstructedAggregate<Tag, S, A, Ev, O>
Properties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
__event | readonly | z.output<Ev> | the event union, read by Entity.Event | types.ts:713 |
__input | readonly | InputOf<S> | - | types.ts:710 |
__instance | readonly | ConstructedAggregate<Tag, S, A, Ev, O> | - | types.ts:714 |
__output | readonly | OutputOf<S, A> | - | types.ts:711 |
entityName | readonly | Tag | - | types.ts:705 |
events | readonly | Ev | the declared event union, for an outbox or an event store's contract | types.ts:709 |
input | readonly | z.ZodObject<PlainOf<S, "input">> | - | types.ts:706 |
output | readonly | z.ZodObject<PlainOf<S, "output"> & A> | - | types.ts:707 |
Methods
inspect()
inspect(state): Result<Inspection<OutputOf<S, A>>, InvalidEntity>;Defined in: types.ts:725
Parameters
| Parameter | Type |
|---|---|
state | unknown |
Returns
Result<Inspection<OutputOf<S, A>>, InvalidEntity>
make()
make<T>(
this,
state,
loaded
): Result<T, InvalidEntity>;Defined in: types.ts:720
a snapshot or a state-based row → aggregate; emits nothing. The version is required: it is what the aggregate's next decision tells the store to expect, so an aggregate can never be saved without one.
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
this | (d) => T |
state | unknown |
loaded | { version: number; } |
loaded.version | number |
Returns
Result<T, InvalidEntity>
replay()
replay<T>(this, events): Result<T, InvalidEntity>;Defined in: types.ts:732
a stored stream → aggregate: parse every event, fold, one make; emits nothing
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
this | (d) => T |
events | unknown |
Returns
Result<T, InvalidEntity>
start()
start<T>(this, event): Result<Decision<T, output<Ev>>, never>;Defined in: types.ts:727
an opening event → the decision that creates the aggregate
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
this | (d) => T |
event | Extract<output<Ev>, { type: K; }> |
Returns
Result<Decision<T, output<Ev>>, never>
Decision
type Decision<A, E> = object;Defined in: types.ts:638
What a command returns: the state, already verified by make; every event decided since the aggregate was loaded; and the version it was loaded at. Only emit and start build one, so a repository that takes a Decision can only be handed events that were folded and checked against every invariant — and it needs nothing else to save one.
events accumulates across chained commands, so saving the last decision of a chain loses none of the earlier ones. expectedVersion is the one the store must still be at: 0 for a new aggregate, the stream length after replay, the row's version after make. The package carries it and never interprets it.
Type Parameters
| Type Parameter |
|---|
A |
E |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
__onlyEmitOrStartMakeADecision | readonly | DecisionKey | types.ts:643 |
events | readonly | readonly E[] | types.ts:640 |
expectedVersion | readonly | number | types.ts:641 |
state | readonly | A | types.ts:639 |
EntityStatic()
type EntityStatic<Tag, S, A, B> = BaseInstance<S, A> & DeepReadonly<OutputOf<S, A>> & object & B;Defined in: types.ts:491
The full static surface Entity(tag)(fields, options?) returns.
Named here, in entity.ts's companion types module, rather than left as the inline Base as unknown as {…} cast at the end of the builder, so it can double as the builder's explicit return-type annotation, and so the package's exported helper types (Input, Output, CreateInput, Patch) have a single surface to read the shapes off. This is why every member below is expressed from S/A/Tag alone instead of the builder's body-local Base/input/output/etc. — those aren't in scope at the annotation position, before the body that declares them.
There are no G/I parameters: the key unions are computed inside the body from the flags S carries — see GeneratedKeys for why they must never move into a parameter list.
Type Parameters
| Type Parameter | Default type |
|---|---|
Tag extends string | - |
S extends Fields | - |
A extends Schemas | - |
B | Record<never, never> |
type new EntityStatic(d): BaseInstance<S, A> & DeepReadonly<OutputOf<S, A>> & object & B;The full static surface Entity(tag)(fields, options?) returns.
Named here, in entity.ts's companion types module, rather than left as the inline Base as unknown as {…} cast at the end of the builder, so it can double as the builder's explicit return-type annotation, and so the package's exported helper types (Input, Output, CreateInput, Patch) have a single surface to read the shapes off. This is why every member below is expressed from S/A/Tag alone instead of the builder's body-local Base/input/output/etc. — those aren't in scope at the annotation position, before the body that declares them.
There are no G/I parameters: the key unions are computed inside the body from the flags S carries — see GeneratedKeys for why they must never move into a parameter list.
Parameters
| Parameter | Type |
|---|---|
d | Sealed<OutputOf<S, A>> |
Returns
BaseInstance<S, A> & DeepReadonly<OutputOf<S, A>> & object & B
Properties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
__createInput | readonly | CreateInputOf<S, GeneratedKeys<S>> | - | types.ts:539 |
__input | readonly | InputOf<S> | phantom carriers, so consumers can recover the shapes for annotations | types.ts:537 |
__instance | readonly | ConstructedInstance<Tag, S, A> & B | the instance type, read by Entity.Instance | types.ts:542 |
__output | readonly | OutputOf<S, A> | - | types.ts:538 |
__patch | readonly | PatchOf<S, A, ImmutableKeys<S>> | - | types.ts:540 |
_zod | readonly | z.ZodType<ConstructedInstance<Tag, S, A> & B>["_zod"] | The zod slots that make the class itself a schema, so it composes directly: z.object({ owner: Organization }), z.array(Organization), or as a field of another entity. Parsing yields a real instance. Only these two are declared, never the full ZodType: that would put a throwing .parse() on every entity beside make, which is the opposite of what this package is for. Wrapping still works through zod's function forms — z.optional(Organization) rather than Organization.optional(). The runtime binds to whichever class the slot is read from, so a schema built from a subclass yields that subclass. The type cannot say so — a property, unlike a method, takes no this parameter to infer the receiver from — so it states the base shape and a caller narrows with instanceof. | types.ts:534 |
~standard | readonly | z.ZodType<ConstructedInstance<Tag, S, A> & B>["~standard"] | - | types.ts:535 |
createInput | readonly | z.ZodObject<Omit<PlainOf<S, "input">, GeneratedKeys<S>>> | - | types.ts:517 |
entityName | readonly | Tag | - | types.ts:511 |
input | readonly | z.ZodObject<PlainOf<S, "input">> | - | types.ts:515 |
output | readonly | z.ZodObject<PlainOf<S, "output"> & A> | - | types.ts:516 |
updateInput | readonly | z.ZodObject<UpdateInputShapeOf<S, A, ImmutableKeys<S>>> | - | types.ts:518 |
Methods
factory()
factory<T>(this, generators): EntityFactory<T, S, { [K in string | number | symbol]: S[K] extends { flags: { generated: true } } ? K : never }[keyof S]>;Defined in: types.ts:548
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
this | (d) => T |
generators | Generators<S, GeneratedKeys<S>> |
Returns
EntityFactory<T, S, { [K in string | number | symbol]: S[K] extends { flags: { generated: true } } ? K : never }[keyof S]>
factoryAsync()
factoryAsync<T>(this, generators): AsyncEntityFactory<T, S, { [K in string | number | symbol]: S[K] extends { flags: { generated: true } } ? K : never }[keyof S]>;Defined in: types.ts:552
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
this | (d) => T |
generators | AsyncGenerators<S, GeneratedKeys<S>> |
Returns
AsyncEntityFactory<T, S, { [K in string | number | symbol]: S[K] extends { flags: { generated: true } } ? K : never }[keyof S]>
inspect()
inspect(state): Result<Inspection<OutputOf<S, A>>, InvalidEntity>;Defined in: types.ts:545
stored data → its data plus the invariants it breaks, never an entity
Parameters
| Parameter | Type |
|---|---|
state | unknown |
Returns
Result<Inspection<OutputOf<S, A>>, InvalidEntity>
make()
make<T>(this, state): Result<T, InvalidEntity>;Defined in: types.ts:543
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
this | (d) => T |
state | unknown |
Returns
Result<T, InvalidEntity>
EntityUnion
type EntityUnion<K, M> = object & Pick<z.ZodType<InstanceOf<M[number]>>, "_zod" | "~standard">;Defined in: union.ts:39
What Entity.union(...) returns: a value, never a constructor.
There is deliberately no new signature. A class's instance type cannot be a union — TS2509: Base constructor return type 'Personal | Business' is not an object type or intersection of object types with statically known members — so a class form could only ever type as the members' shared root, which is both unable to narrow and redundant with the root the author already named. It shipped in 0.4.0 and was removed in #57. TS2507 at the declaration is the replacement, and it fires where the mistake is written.
Type Declaration
| Name | Type | Description | Defined in |
|---|---|---|---|
__instance | InstanceOf<M[number]> | the exact member union, read by Entity.Instance | union.ts:48 |
discriminant | K | - | union.ts:40 |
input | z.ZodType<z.output<M[number]["input"]>, z.input<M[number]["input"]>> | - | union.ts:45 |
members | M | - | union.ts:41 |
output | z.ZodType<z.output<M[number]["output"]>, z.input<M[number]["output"]>> | - | union.ts:46 |
inspect() | (state) => Result<Inspection<output<M[number]["output"]>>, InvalidEntity> | dispatches on the discriminant like make, then reports — see an entity's inspect | union.ts:51 |
make() | (state) => Result<output<M[number]>, InvalidEntity> | - | union.ts:49 |
Type Parameters
| Type Parameter |
|---|
K extends string |
M extends readonly UnionMember[] |
FieldSpec
type FieldSpec<T, F> = object;Defined in: field.ts:11
One flagged field: the schema, held — never impersonated. Anything standing in front of an entity-class field breaks make, which constructs through this (TypeError: Ctor is not a constructor — measured), so the spec object is the only shape a marker may take.
Type Parameters
| Type Parameter |
|---|
T extends z.core.$ZodType |
F extends Flags |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
flags | readonly | F | field.ts:13 |
schema | readonly | T | field.ts:12 |
Inspection
type Inspection<D> = object;Defined in: types.ts:228
What inspect returns: a stored row's data, and the rules it breaks.
Deliberately not an entity. data is plain frozen data — no _tag, no update, no sameIdentityAs, no class body — so it cannot be handed to a command that expects the entity, and ignoring violations cannot smuggle a row that breaks today's rules into the command model. The way back is a migration followed by make, which is strict. violations are the issues make would have failed with, params.code and all, so Entity.codeOf reads them.
Named, and exported from index.ts, so a consumer's emitted declarations print Inspection<…> rather than spelling the object out at every inspect call they return from.
Type Parameters
| Type Parameter |
|---|
D |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
data | readonly | DeepReadonly<D> | types.ts:229 |
violations | readonly | SchemaIssues | types.ts:230 |
MergedComputed
type MergedComputed<A, A2> = Omit<A, keyof A2> & A2;Defined in: types.ts:396
A root's computed map merged with a variant's — what extend hands EntityStatic as its A.
Omit<A, keyof A2> & A2, never A & A2: the runtime spread lets a variant's computed key win, and a plain intersection would type a redefined key as Upper & Lower while the value is Lower.
Named rather than written inline at extend's return type, which was measured to emit a dangling reference: with A = Record<never, never> — a root declaring no computed, which is the default — TypeScript 5.9.3 wrote the unsubstituted Omit<Record<never, never>, keyof A2> & Record<never, never> into the consumer's .d.ts, where the consumer's own compiler rejected it with TS2304: Cannot find name 'A2'. TypeScript 7.0.2 substitutes the same position correctly. Naming it is only half the fix: the emitter writes the name only because index.ts exports it, and unexporting it was measured to expand the alias structurally and bring the identical dangling A2 straight back. See the export list there, and do not un-export it.
The fields half of the same merge is MergedFields, below.
Type Parameters
| Type Parameter |
|---|
A extends Schemas |
A2 extends Schemas |
MergedFields
type MergedFields<S, S2> = Omit<S, keyof S2> & S2;Defined in: types.ts:429
A root's field map merged with a variant's — what extend hands EntityStatic as its S.
Omit<S, keyof S2> & S2, never S & S2, for MergedComputed's reason one map over: the runtime spread is { ...parent.fields, ...nextFields }, so a variant redeclaring an inherited field wins, and a plain intersection would type that key as ZodBranded<Parent> & ZodBranded<Child> while the schema held is the child's alone.
Named and exported from the start for the reason measured on MergedComputed: written inline, the 5.9.3 emitter copied that alias's A2 through unsubstituted and consumers failed with TS2304. This is the same alias in the same position, so it was never written inline here and the S2 spelling of that failure has not been observed — it is inferred from the A2 one, not a second measurement. See the export list in index.ts, and do not un-export it.
Serialised width was the recorded reason this half was deferred, since every variant pays it where MergedComputed is paid only by one declaring computed. Measured rather than assumed: the emitter writes the alias by reference, so the billing fixture's index.d.ts grew 10,061 → 10,145 bytes — 42 per variant, against the 274,048 an unnamed type expands to — and all four typecheck steps stayed clean on both compilers. The TS7056 budget is serialised characters, and a named alias barely spends it.
NoRedeclaredKeys now forbids keyof S ∩ keyof S2 outright, so the child-wins branch here is unreachable at compile time; the alias stays as the runtime-honest spelling, and base.test-d.ts guards the rejection itself rather than the merge's resolution, in case the forbid ever loosens.
Type Parameters
| Type Parameter |
|---|
S extends Fields |
S2 extends Fields |
Sealed
type Sealed<D> = D & object;Defined in: types.ts:290
Type Declaration
| Name | Type | Defined in |
|---|---|---|
__useMakeOrFactoryInstead | ConstructionKey | types.ts:290 |
Type Parameters
| Type Parameter |
|---|
D |
UnionMember
type UnionMember = object & z.core.$ZodType;Defined in: union.ts:12
The part of an entity a union needs. Typed loosely — EntityStatic is generic in the entity's own shape, and a union has to accept any of them.
Type Declaration
| Name | Type | Defined in |
|---|---|---|
entityName | string | union.ts:13 |
input | z.ZodObject<z.core.$ZodLooseShape> | union.ts:14 |
output | z.ZodObject<z.core.$ZodLooseShape> | union.ts:15 |
inspect() | (state) => Result<Inspection<unknown>, InvalidEntity> | union.ts:17 |
make() | (state) => Result<unknown, InvalidEntity> | union.ts:16 |
Functions
Entity()
function Entity<Tag>(tag): <S, A>(fields, options?) => EntityStatic<Tag, S, A>;Defined in: entity.ts:70
class X extends Entity("X")({ …fields }) {}
Curried on the tag so it reads next to the class name it labels, ahead of the field map. The tag is a non-enumerable instance property: it exists for P.tag matching and never reaches the wire.
Type Parameters
| Type Parameter |
|---|
Tag extends string |
Parameters
| Parameter | Type |
|---|---|
tag | Tag |
Returns
<S, A>(fields, options?) => EntityStatic<Tag, S, A>
