Pattern Matching
Innerhalb des onReceive eines Actors empfängst du einen Wert vom
Nachrichten-Union-Typ des Actors und dispatched auf seine Form.
Diese Codebasis verwendet dafür
ts-pattern — spezifisch
das match(value).with(...).exhaustive()-Idiom. Drei Vorteile
gegenüber handgeschriebenen if/else-Leitern:
- Compile-time-Exhaustiveness. Vergiss ein
kind, der Compiler scheitert — kein stiller Durchfall. - Typ-Narrowing innerhalb jedes Arms. Keine
as-Casts, keine manuellenif (msg.kind === ...)-Guards innerhalb von Handlern. - Lesbar für nicht-triviale Unions. Bei 5+ Varianten bleibt
die
match-Form scannbar; eineif/else if-Leiter tut das nicht.
Ein minimales Beispiel
Abschnitt betitelt „Ein minimales Beispiel“import { Actor } from 'actor-ts';import { match } from 'ts-pattern';
type Cmd = | { readonly kind: 'inc' } | { readonly kind: 'dec' } | { readonly kind: 'set'; readonly to: number } | { readonly kind: 'reset' };
class Counter extends Actor<Cmd> { private count = 0;
override onReceive(cmd: Cmd): void { match(cmd) .with({ kind: 'inc' }, () => { this.count++; }) .with({ kind: 'dec' }, () => { this.count--; }) .with({ kind: 'set' }, (m) => { this.count = m.to; }) .with({ kind: 'reset' }, () => { this.count = 0; }) .exhaustive(); }}Innerhalb des set-Arms ist m automatisch als
{ kind: 'set'; to: number } typisiert — der Diskriminator
verengt den Typ ohne Cast.
.exhaustive() am Ende ist der Compile-time-Check: wenn du
später { kind: 'double' } zu Cmd hinzufügst und den passenden
.with({ kind: 'double' }, ...)-Arm vergisst, weigert sich
TypeScript zu kompilieren und zeigt auf den
.exhaustive()-Aufruf. Der Build fängt das Versäumnis ab; du
shippst nie eine still verworfene Nachricht.
Die kind-Konvention
Abschnitt betitelt „Die kind-Konvention“Die Discriminated-Union-Konvention des Frameworks ist
kind: string, klein geschrieben, kebab-case für mehrere
Wörter:
type AccountCmd = | { readonly kind: 'deposit'; readonly amount: number } | { readonly kind: 'withdraw'; readonly amount: number } | { readonly kind: 'close-account' } | { readonly kind: 'get-balance'; readonly replyTo: ActorRef<number> };Warum kind und nicht type oder tag:
typewürde mit TypeScriptstype-Keyword bei Typ-Narrowing-Lesevorgängen kollidieren (lästig, nicht kaputt).tagist in Ordnung, aber weniger selbsterklärend alskind.- JVM-typisierte Actor-Frameworks verwenden Sealed-Trait-Subklassen;
in TS-Land ist
kinddas häufigste Community-Idiom.
Sich an die Konvention zu halten, zahlt sich an drei Stellen aus:
die Exhaustiveness-Prüfung von ts-pattern funktioniert, der
Serializer kann über die Wire-Grenze per kind dispatchen, und
Editoren können auf der Literal-Union autovervollständigen.
Häufige ts-pattern-Features
Abschnitt betitelt „Häufige ts-pattern-Features“Object-Pattern-Narrowing
Abschnitt betitelt „Object-Pattern-Narrowing“match(cmd) .with({ kind: 'deposit', amount: P.number }, (m) => { /* m.amount: number */ }) .with({ kind: 'deposit', amount: 0 }, () => { /* Zero-Amount-Deposit */ }) .otherwise(() => { /* Fallthrough */ });Patterns können auch auf Feldwerten verfeinern — amount: 0 matcht
nur, wenn der Betrag wörtlich null ist, und fällt sonst auf den
allgemeineren P.number-Arm durch.
P ist der Pattern-Builder-Namespace von ts-pattern. Nützliche
Primitives:
| Pattern | Was es matcht |
|---|---|
P.string | Jeder String |
P.number | Jede Zahl |
P.array(P.string) | Array von Strings |
P.union('a', 'b') | Eines der Literale |
P.when((x) => x > 0) | Predicate-Guard |
P.any | Wildcard |
Siehe die ts-pattern-Docs für den vollen Set.
Einen Wert zurückgeben
Abschnitt betitelt „Einen Wert zurückgeben“match ist ein Ausdruck — du kannst Werte zurückgeben:
const reply = match(cmd) .with({ kind: 'get' }, () => this.count) .with({ kind: 'inc' }, () => this.count + 1) .exhaustive();Innerhalb des onReceive eines Actors verwendest du es meist für
seine Seiteneffekte (tell an eine Reply-to-Ref, Felder mutieren).
Aber für typed-Actor-Behavior-Returns ist die Ausdrucksform
bequem.
.otherwise vs .exhaustive
Abschnitt betitelt „.otherwise vs .exhaustive“match(cmd) .with({ kind: 'inc' }, () => { ... }) .with({ kind: 'dec' }, () => { ... }) .otherwise(() => this.log.warn('unknown cmd')); // Runtime-Fallback.otherwise(fn) fängt alles, was nicht gematcht wurde. Verwende
es für Actors, die ein Subset eines breiteren Nachrichtentyps
behandeln — z.B. ein PersistentActor, der bewusst Commands
ignoriert, die er noch nicht behandeln kann.
Default zu .exhaustive(). .otherwise() deaktiviert die
Exhaustiveness-Prüfung; greife bewusst dazu, wenn “ignoriere den
Rest” die richtige Semantik ist.
Wann ein einfaches if zu bevorzugen ist
Abschnitt betitelt „Wann ein einfaches if zu bevorzugen ist“Für Unions mit zwei oder drei Varianten liest sich eine einfache
if-Kette sauberer:
override onReceive(cmd: Cmd): void { if (cmd.kind === 'inc') this.count++; else if (cmd.kind === 'dec') this.count--; else cmd.replyTo.tell(this.count);}TypeScript verengt innerhalb jedes Branchs — der else-Arm sieht
automatisch { kind: 'get'; replyTo: ... }. Du verlierst den
.exhaustive()-Check, aber bei 2-3 Varianten sind die Kosten
niedrig.
Die Faustregel: verwende match ab 4+ Varianten, besonders
wenn Arms Destructuring brauchen ((m) => m.amount).
ts-pattern als Runtime-Abhängigkeit
Abschnitt betitelt „ts-pattern als Runtime-Abhängigkeit“ts-pattern ist eine Peer-Abhängigkeit des Frameworks — du installierst es selbst:
bun add ts-pattern# oder: npm install ts-patternDas Framework verlangt nicht, dass du es verwendest; du kannst
onReceive als einfaches Switch schreiben. Aber jedes
Codebeispiel in den Docs nimmt an, dass import { match } from 'ts-pattern'
verfügbar ist, weil das die Konvention ist, der die Codebasis folgt.
Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- Nachrichten — die
Discriminated-Union-Form, auf der
matchdispatched. - Actor — die
onReceive-Signatur, innerhalb derermatchläuft. - ts-pattern-Dokumentation — die volle Feature-Schnittstelle der Bibliothek.
- Typed Actors — die typed-API drückt manche Patterns direkter über Behaviors aus.