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 (message.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 Command = | { readonly kind: 'increment' } | { readonly kind: 'decrement' } | { readonly kind: 'set'; readonly to: number } | { readonly kind: 'reset' };
class Counter extends Actor<Command> { private count = 0;
override onReceive(command: Command): void { match(command) .with({ kind: 'increment' }, () => this.onIncrement()) .with({ kind: 'decrement' }, () => this.onDecrement()) .with({ kind: 'set' }, (m) => this.onSet(m)) .with({ kind: 'reset' }, () => this.onReset()) .exhaustive(); }
private onIncrement(): void { this.count++; } private onDecrement(): void { this.count--; } private onSet(command: Extract<Command, { kind: 'set' }>): void { this.count = command.to; } private onReset(): void { this.count = 0; }}Jeder Arm ist ein dünner Aufruf einer privaten onXxx-Handler-Methode
— die Hausregel (siehe unten), nie ein Inline-Rumpf. Das Narrowing
überlebt die Übergabe: Der Parameter von onSet ist als
Extract<Command, { kind: 'set' }> typisiert, sodass command.to ohne
Cast eine number ist.
.exhaustive() am Ende ist der Compile-time-Check: wenn du
später { kind: 'double' } zu Command 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.
Jeden Arm in einen privaten Handler auslagern
Abschnitt betitelt „Jeden Arm in einen privaten Handler auslagern“Beachte das minimale Beispiel oben: Jeder Arm ist ein dünner Aufruf
einer privaten onXxx-Methode, kein Inline-Rumpf. Das ist die
Hausregel — jeder .with(…) und jedes .otherwise(…) delegiert,
auch ein Einzeiler, ohne Ausnahme. Der Matcher bleibt eine scannbare
Dispatch-Tabelle; die Logik lebt in benannten Handlern.
- Benenne den Handler
on+ PascalCase deskind—onIncrement,onData,onCloseAccount; der.otherwise(…)-Fallback heißtonUnhandled. - Typisiere den Parameter als die verengte Variante
(
Extract<Command, { kind: 'set' }>), damit der Handler das Narrowing von ts-pattern behält; lass ihn bei nutzlastfreien Kinds weg.
Das ist eine harte Regel in der AGENTS.md des Repos (Code style).
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 AccountCommand = | { 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(command) .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(command) .with({ kind: 'get' }, () => this.onGet()) // onGet(): number .with({ kind: 'next' }, () => this.onNext()) // onNext(): number .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(command) .with({ kind: 'increment' }, () => this.onIncrement()) .with({ kind: 'decrement' }, () => this.onDecrement()) .otherwise((m) => this.onUnhandled(m)); // Runtime-Fallback.otherwise(handler) 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.
ts-pattern als Runtime-Abhängigkeit
Abschnitt betitelt „ts-pattern als Runtime-Abhängigkeit“ts-pattern ist eine reguläre Runtime-Abhängigkeit des Frameworks — es wird automatisch zusammen mit actor-ts installiert, du musst also nichts zusätzlich hinzufügen.
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: jeder Dispatch auf eine
eingehende Nachricht, ein Event oder ein Command läuft über
match, und jeder Arm delegiert an einen onXxx-Handler.
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.
