All you ever wanted to know about tag-associated type-checking.
In the tutorial on tag typing, we have motivated the system and shown briefly how to use it. This guide covers all the tricks, corner-cases and design reasons.
There are three object types involved with the tag query system. They form a hierarchy:
Tag is the
most specific one,
Tags is a like set of
Tag objects, and
Where is like a collection of
A single tag, tied to the type of events it may be attached to. Using TypeScript’s union types, we are unrestricted in the number of associated events:
Tag can be used both for emission and for event selection:
Tags is the next more general type. It represents a "set of tags."
A single tag may turn into
Tags in two ways.
withId, we create a set of tags that contains both the original tag, as well as a postfixed version:
Tags('foo-or-bar-event', 'foo-or-bar-event:id'). This is so that the set of all foo-bar-events remains selectable via the general tag. There are no prefix-searches on tags – they can only be matched exactly. So we must attach both.
Tagsobject containing them all:
Tag('a').and(Tag('b'))is equivalent to
Tags object can be used both for emission and for event selection, just like a single
- When used for selection, it will match events that have all the tags.
Tags('foo', 'bar')requires both
barto be present on an event. (The event may have more tags than that and will still match.)
- When used for emission, all the tags are attached to the emitted event.
Where is the most general type, expressing some arbitrary event selection. The field
where of a
Fish object accepts this type, and hence accepts
Tags as well, since they are extensions
Tag<E1>('tag 1').or(Tag<E2>('tag 2')): Where<E1 | E2> will match events that have
tag 1 and also
events that have
tag 2. It suffices if one of both tags is present. If both are present, the
event is also selected. And, as in all cases, the event may also have more tags.
Inspect your Queries
Since Pond 2.2 you can call
Tag objects to find out what your query does
under the hood, at a glance.
Where, you can not do this:
You have to fall back to the normalized form:
We do not support the first version, in order to guard against mistakes of the following kind:
tagA.or(tagB).and(tagC) instead of
tagA.or(tagB.and(tagC)) – just one
misplaced bracket and the whole query is significantly altered, perhaps selecting nothing at all,
because events with tagA and events with tagC don’t overlap. (According to normal boolean logic
rules, one might expect the
and to bind stronger than the
or, but this is very hard to mimic in
TypeScript evaluation order where
or will always be called first!)
Now for a closer look on how the associated event type behaves when operating with tags. Say we concatenate two tags into a set:
fooBarTag may contain both:
fooTag can only contain
EventFoo is the only common type between the two.
and does detect this
type intersection between both arguments, and narrows down the result’s associated type accordingly.
fooTag is only attached to
EventFoo instances, and we require
fooTag to be
present on the selected events, we can expect to find no
EventBar instances anymore, when
requiring both tags at once.
Now let’s consider both sides of the event system, producer (
Pond.emit) and consumer (
- The Fish is required to handle all events it may possibly receive.
onEventmust not take a type more narrow than the type associated with
EventBarfrom the type now makes the implementation easier: the impossible case of receiving an
EventBardoes not have to be considered anymore by
- Events passed into
Pond.emitmust not be tagged with tags that do not declare a fitting type association. Emitting a
BarEventhas been dropped from the associated type.
The OR-case is somewhat the reverse of the AND-case. The type is widened instead of narrowed.
Logically, since we require only either of the two tags to be present on the event, we will receive
events of both associated types. A Fish running on this event set must handle both types in its
Where statement cannot be used for emission. The intent is unclear: What does it mean to emit
an event tagged with one tag OR another tag? And also the type requirement does not match: Even though the
associated type is
EventFoo | EventBar, it’s actually incorrect to tag either of the events with
fooTag may not tag
barTag may not tag
fooTag.and(barTag) fittingly infers
Tag<never> in this case, meaning it will yield no events and
allow no events to be emitted, either.)
All this type-checking is in effect only at compile-time. Events are persistent, and possibly shared
between different programs, or versions of your program. If you emitted an
attached in the past, it will be passed to
Hence you should take care to implement your
onEvent somewhat defensively. Below, we outline some
good architecture practices to keep you safe.
In any case, if you want bullet-proof safety, consider using io-ts for runtime type checks!
It is strongly recommended you declare each of your tags only once: statically, with fixed associated types. And then always reference the canonical instance. That is:
When changing your application, you should then take care to never narrow an associated type – because potentially there are old persisted events of the associated type you are removing. Consumers must stay aware of that.
As an example, let us assume we want to incompatibly change the shape of
Keep in mind that no matter how thoroughly you try to purge
EventFoo from your application code,
instances may be persisted in your ActyxOS swarm! So we recommend that your application code does
not forget about
EventFoo at all.
fooBarTag will still allow emission of
EventFoo. To prevent unwitting producers from
doing this, you should design your module something like this:
If you want to spare new consumers the burden of supporting the old
EventFoo, and those consumers
do not care about missing out on the old data, consider introducing a new tag, as well:
Finally, stop exporting any old
emitFooEvent function you may have offered.
When giving tags inline, types can often be automatically inferred. This is nice for prototyping. Beware though that it’s the exact reverse of static type-safety guarantees. The compiler simply trusts you that things will work out.