Typed overview
Este conteúdo não está disponível em sua língua ainda.
actor-ts ships two layers for the same actor model:
| API | Style | Source of truth |
|---|---|---|
| Untyped (the fundamentals) | Subclass Actor<TMessage>, override onReceive, mutate this.state. | This is the lower-level, more flexible API. |
| Typed (this section) | Compose Behavior<T> values via Behaviors.receive(...), return next behavior from handlers. | A functional facade on top of the untyped engine. |
The framework runs both on the same dispatcher, the same mailbox, the same supervision tree. The choice is expressive style, not capability.
A direct comparison
Section titled “A direct comparison”Same counter, two ways:
Untyped:
import { match } from 'ts-pattern';import { Actor, ActorSystem } from 'actor-ts';
type IncrementCommand = { kind: 'increment' };type DecrementCommand = { kind: 'decrement' };type Command = IncrementCommand | DecrementCommand;
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()) .exhaustive(); }
private onIncrement(): void { this.count++; } private onDecrement(): void { this.count--; }}
const system = ActorSystem.create('demo');const counter = system.spawnAnonymous(Counter);Typed:
import { match } from 'ts-pattern';import { ActorSystem, Behaviors } from 'actor-ts';
type IncrementCommand = { kind: 'increment' };type DecrementCommand = { kind: 'decrement' };type Command = IncrementCommand | DecrementCommand;
const counter = (n: number) => Behaviors.receive<Command>((_context, command) => match(command) .with({ kind: 'increment' }, () => counter(n + 1)) .with({ kind: 'decrement' }, () => counter(n - 1)) .exhaustive());
const system = ActorSystem.create('demo');const ref = system.spawnTypedAnonymous(counter(0));Same runtime semantics; different ergonomics:
- The untyped form keeps
countin a field, mutates it, returnsvoid. Classic OO. - The typed form passes
nas a parameter to the next behavior. No mutable state — the next behavior captures the new count in its closure.
What typed gets you
Section titled “What typed gets you”Four things the typed API does that untyped doesn’t:
- State as parameters, not fields. Each behavior captures
its state in a closure; the “next state” is what you return
from the handler. Less footgun-prone than
this.xmutation, especially when refactoring. - Pure-functional message handling. Handlers are
(context, message) => Behavior<T>— no side effects onthis, no imperative state machine. Easier to test in isolation; a behavior is just a value. Behaviors.same/Behaviors.stoppedsentinels. The handler return type is the actor’s state-transition decision, so “keep the same behavior” and “stop the actor” are values you return rather than imperativecontext.stopSelf()calls.- Cast-free child spawning.
context.spawn(behavior)returnsActorRef<U>typed to the child’s message type, derived from the behavior. Noas ActorRef<...>casts at spawn time.
What untyped gets you
Section titled “What untyped gets you”Three things the untyped API does more cleanly:
- Mutable state across messages. When the actor has a
complex aggregate (a
Map, a buffer, a state machine with five fields), threading it through every behavior return gets awkward. Mutatingthisis fine. - Lifecycle hooks (
preStart,postStop,preRestart). The typed API exposes these asSignalevents on areceiveWithSignalhandler, which is a bit more verbose than overriding a method. - Direct interop with the rest of the framework. Most
examples in the docs use the untyped form. Most cluster
extensions (sharding, singleton, pubsub) are typed in terms of
ActorRef<T>either way — but their internals are untyped classes, and that occasionally leaks at the seams.
When to pick which
Section titled “When to pick which”Pick typed when:
- The actor is a state machine and you want phases to be different behaviors (different valid-message sets per phase). The typed form makes the transitions explicit returns.
- You’re coming from a typed actor API on another runtime, from Cats Effect, or fp-ts and the functional style feels native.
- You want compiler-checked state-transition logic — typed behaviors compose at type level.
Pick untyped when:
- The actor has substantial mutable state (a cache, a buffer, a set of subscribers) and threading it through behaviors is more ceremony than value.
- You’re learning the actor model and don’t want a second layer of abstraction in the way.
- You want one-to-one parity with classic untyped actor APIs from other libraries you’ve used.
You can mix them. An untyped parent can spawn typed children
via context.spawnTypedAnonymous(behavior), and a typed parent spawns
children via context.spawn(behavior). Refs are interoperable — both
forms expose ActorRef<T> with the same tell(message) signature, so
typed and untyped actors can message one another across the boundary.
What the typed surface looks like
Section titled “What the typed surface looks like”A handful of building blocks:
| Combinator | What it does |
|---|---|
Behaviors.setup(context => behavior) | Run once with the context; returns the initial behavior. Akin to a “constructor.” |
Behaviors.receive((context, message) => behavior) | Standard message handler. Returns the next behavior. |
Behaviors.receiveMessage(message => behavior) | Shortcut when you don’t need the context. |
Behaviors.receiveWithSignal(handler, signalHandler) | Add a lifecycle-signal handler (postStop, preRestart, terminated). |
Behaviors.withTimers(timers => behavior) | Capture the per-actor TimerScheduler in a closure. |
Behaviors.withStash(capacity, stash => behavior) | Capture a StashBuffer<T> with the given capacity. |
Behaviors.supervise(behavior).onFailure(strategy) | Wrap a behavior with a supervisor strategy. |
Behaviors.intercept(behavior, interceptor) | Run an interceptor before every message — observe, transform, or drop it. |
Behaviors.monitor(observer, behavior) | Forward every message to another actor before handling it. |
Behaviors.logMessages(behavior, options?) | Log every message before handling it. |
Behaviors.same | Sentinel: keep the current behavior. |
Behaviors.stopped | Sentinel: stop the actor. |
Behaviors.unhandled | Sentinel: this message is unhandled (goes to dead letters). |
Behaviors.empty | Sentinel: no-op handler. |
Behaviors.ignore | Sentinel: silently drop every message. |
Each page in this section drills into one slice — see Behaviors for the combinators, Typed actor for the engine that hosts a behavior, Spawn typed for the three spawn helpers.
Where to next
Section titled “Where to next”- Behaviors — the full DSL.
- Typed actor — what runs a Behavior at runtime.
- Spawn typed —
system.spawnTyped,context.spawnTyped,typedActor. - Fundamentals overview — the untyped concept map; many ideas carry over.
