Skip to content

Latest commit

 

History

History
222 lines (174 loc) · 9.24 KB

typescript.md

File metadata and controls

222 lines (174 loc) · 9.24 KB
title TypeScript
description Define schemas and derive XState types.

Define schemas with setup(...). XState infers context, events, input and output from them. State contracts can additionally declare state-node structure and defaults, including type, initial, history, target and id.

Enable TypeScript's strict mode. Keep schemas next to actor logic so runtime validation and inferred types describe the same contract.

import { z } from 'zod';
import { setup } from 'xstate';

const machine = setup({
  schemas: {
    context: z.object({ count: z.number() }),
    events: {
      increment: z.object({})
    },
    internalEvents: {
      tick: z.object({ count: z.number() })
    },
    input: z.object({ initialCount: z.number() })
  }
}).createMachine({
  context: ({ input }) => ({ count: input.initialCount }),
  initial: 'active',
  states: { active: {} }
});

A declared input schema whose inferred type does not accept undefined makes input required: createActor(machine) is a type error until you pass { input } (or restore a snapshot with restoreActor(...)). Framework hooks such as useActor, useActorRef and useMachine inherit the same requirement. createActorContext from @xstate/react is the exception: input stays optional because its options are merged from the context defaults and the <Provider options> prop, so a missing input surfaces at runtime as an initialization error instead.

Structural state contracts are checked only when declared. For example, type: 'parallel' forbids initial, while type: 'compound' requires one. setup(...) can supply those defaults, so the machine config may omit them. Setups that declare only schemas do not impose these structural checks. A state-level schemas.context is intersected with the root context schema, so it can declare only the fields that state narrows.

XState infers snapshot.value and valid snapshot.matches(...) patterns from nested and parallel states. Match a nested state with an object such as { b: 'x' } or a dot path such as 'b.x'.

You can export machines and state configs with their inferred types, without adding explicit type annotations.

For an actor used in one state, invoke: s.createInvoke({ src, input, onDone }) infers input and completion output from src. Keep the helper inline in s.createMachine(...) to also infer that state's narrowed context, ancestor context, state input and transition target requirements. The helper works alongside named actor sources. See typed inline invokes.

A bare inline invoke: { src: logic, onDone } cannot correlate its source with the output; with no registered actor sources it has event.output: unknown. Use s.createInvoke(...) or a source registered in setup({ actors }) when reading the output. This is especially important for generated machine code: the bare form cannot infer the output type from src.

An async function can be used directly as src. Declare schemas.input, schemas.output, and schemas.error on the invoke to type the function and its lifecycle handlers together. Without schemas.output, onDone.event.output infers from the async return value.

Public schema event keys create typed methods on actor.trigger; internal schema keys do not appear in the public trigger namespace.

actor.trigger.increment();

Event types narrow inside transitions:

on: {
  search: ({ event }) => {
    event.query; // string
  }
}

Use assertEvent(...) only when shared code must narrow a union to one or more known event types.

Typestates

State-level schemas.context declarations describe the context valid in each state. State functions receive the narrowed type, transitions into a state must satisfy its context contract, and snapshot.matches(...) narrows context when reading a snapshot. See Typestates for declaration, transition and runtime validation examples.

Checked event keys

When schemas.events is declared, each key in an on map must match a declared event type. Matching wildcards ('*', 'user.*') and known reserved machine descriptors are allowed; misspelled or invented reserved prefixes are rejected. Without schemas.events, any key is accepted.

An empty schemas.events: {} means the machine has no public events. Final state output callbacks still receive the declared context and input types.

on: {
  toggel: { target: 'active' }
  // Type error: Event type 'toggel' is not declared in schemas.events.
}

Known reserved descriptors in on carry their actual event payloads: actor completion, error and snapshot events; execution errors; state completion; delayed events; and state or actor timeouts. Bare xstate.error.actor handlers receive event.error and event.actorId. Suffixed v5 descriptors such as xstate.done.actor.fetchUser never match, because v6 lifecycle event types carry the id in a payload field; createMachine throws when on uses one. Wildcards over a category type, such as xstate.done.actor.*, still match. Use xstate.error.actor and filter event.actorId explicitly when handling a particular child.

Child completion events

Children declared in schemas.children add their xstate.done.actor and xstate.error.actor events to the event union seen by entry, exit, guards and transition functions. event.actorId is the declared child id, and event.output is that child's output type:

entry: ({ event }) => {
  assertEvent(event, 'xstate.done.actor');
  event.actorId; // 'fetch'
  event.output; // output of the logic declared for `fetch`
}

Without schemas.children, these events are not added. on handlers still narrow to their own event type.

Async logic errors

createAsyncLogic({ schemas: { error } }) types the actor's error snapshot field and event.error in the invoking machine's onError. The schema is type-only. Without it, the error is unknown:

const fetchUser = createAsyncLogic({
  schemas: { error: z.object({ code: z.string() }) },
  run: async () => ({ name: 'David' })
});

// in an invoke of fetchUser
onError: ({ event }) => event.error.code; // string

Checked delay names

When delays are declared with setup({ delays }) or createMachine({ delays }), each after key must be a declared delay name, a number of milliseconds, or a duration string such as '5s'. Without declared delays, any key is accepted.

after: {
  retryDelya: { target: 'retrying' }
  // Type error: Delay 'retryDelya' is not declared in delays.
}

Runtime validation

Experimental: standardSchemaValidator(), ActorValidationError and the other xstate/validation exports may change in a minor release.

Runtime validation is opt-in. Install the Standard Schema validator from the separate xstate/validation entry point:

import { setup } from 'xstate';
import { standardSchemaValidator } from 'xstate/validation';
import { z } from 'zod';

const machine = setup({
  validator: standardSchemaValidator(),
  schemas: {
    input: z.object({ initialCount: z.number() }),
    context: z.object({ count: z.number() }),
    events: {
      increment: z.object({ by: z.number() })
    },
    internalEvents: {
      tick: z.object({ count: z.number() })
    }
  }
}).createMachine({
  context: ({ input }) => ({ count: input.initialCount })
});

The validator checks input and public or internal events before calculation, then checks stable context, active state schemas, child slots, delayed raised events, emitted events and final output before effects run. Invalid actor-produced values error the actor; pure calculations throw an ActorValidationError. Invalid incoming events are rejected without changing or erroring the actor and reported through onRejectedEvent. See validation failures.

Validation is synchronous and assertion-only: async validation is rejected. Parsed schema results are discarded. Type-changing transformations are rejected by the types, but same-type transformations cannot be detected statically; normalize values before sending them. Unknown incoming events are rejected, and unknown emitted events error the actor, when a corresponding schema map exists; use unknownEvents: 'ignore' or unknownEmitted: 'ignore' for open protocols.

Derived setups inherit the validator. Replace it or use validator: undefined to disable it. Persistence, restoration, immediate raised events, action and guard parameters, static meta and tags, and snapshot.can() are not validation boundaries.

Type helpers

Helper Derives
ActorRefFrom<T> Actor reference type.
SnapshotFrom<T> Snapshot type.
EventFromLogic<T> Accepted event type.
InputFrom<T> Input type.
OutputFrom<T> Final output type.

TypeScript cheatsheet

type Actor = ActorRefFrom<typeof machine>;
type Snapshot = SnapshotFrom<typeof machine>;
type Event = EventFromLogic<typeof machine>;
type Input = InputFrom<typeof machine>;
type Output = OutputFrom<typeof machine>;