#3 Type XState with the Typegen
Published on
Use XState's Typegen to automatically generate strong types for your machines by enabling it in your code, naming actions, delays, guards, and services for better TypeScript integration. Rely on Stately's VS Code extension or XState CLI for development and production builds.
Note
The Typegen was designed for XState v4 and is no longer necessary for XState v5. See the new official recommendation about TypeScript.
Hey there!
Typegen is the official and recommended way to type your machines. It works either with Stately’s VS Code extension or with XState’s CLI.
Typegen is an opt-in that needs to be allowed on each machine.
ts
// appMachine.tsimport { createMachine } from "xstate";const appMachine = createMachine({tsTypes: {},});
Once activated by creating a tsTypes
property in the machine definition and saving the file, a tool will generate an appMachine.typegen.ts
file, which will automatically be referenced in the file where the machine is defined to import the generated types.
ts
// appMachine.tsimport { createMachine } from "xstate";const appMachine = createMachine({tsTypes: {} as import('./appMachine.typegen.ts').Typegen0,});
permalinkThe Typegen output
Say we have a small state machine that waits for the SUBMIT
event to submit data to the server and then returns to the idle
state.
ts
// appMachine.tsconst appMachine = createMachine({tsTypes: {} as import("./appMachine.typegen").Typegen0,states: {idle: {on: {SUBMIT: "submitting",},},submitting: {invoke: {src: "submitDataToServer",onDone: {target: "idle",},},},},});
The Typegen will generate a file with important information that can’t be inferred otherwise.
ts
// appMachine.typegen.tsexport interface Typegen0 {"@@xstate/typegen": true;internalEvents: {"done.invoke.(machine).submitting:invocation[0]": {type: "done.invoke.(machine).submitting:invocation[0]";data: unknown;__tip: "See the XState TS docs to learn how to strongly type this.";};"xstate.init": { type: "xstate.init" };};invokeSrcNameMap: {submitDataToServer: "done.invoke.(machine).submitting:invocation[0]";};missingImplementations: {actions: "assignServerResponseToContext";delays: never;guards: never;services: "submitDataToServer";};eventsCausingActions: {assignServerResponseToContext: "done.invoke.(machine).submitting:invocation[0]";};eventsCausingDelays: {};eventsCausingGuards: {};eventsCausingServices: {submitDataToServer: "SUBMIT";};matchesStates: "idle" | "submitting";tags: never;}
A few things can be said about the content of this file.
permalinkmatchesStates
matchesStates
is a type union of all states in the machine. It’s used to strongly type the parameter taken by state.matches()
. If the state machine had compound states — that is, states with child states — it would generate string paths for them, too: submitting.firstStep
.
permalinkeventsCausingActions
, eventsCausingDelays
, eventsCausingGuards
, and eventsCausingServices
eventsCausingActions
, eventsCausingDelays
, eventsCausingGuards
, and eventsCausingServices
are objects describing which events trigger actions, delays, guards, and services defined as strings in the machine. This allows XState to type the event
parameter they receive correctly. For this machine, it means that if the SUBMIT
event had a payload, the submitDataToServer
service would be able to access it:
ts
// appMachine.tsconst appMachine = createMachine({schema: {events: {} as {type: "SUBMIT";data: string;}}}, {services: {submitDataToServer: (context, event) => {// This is strongly typed 👇event.type === "SUBMIT";// This is too 👇typeof event.data === "string";},},});
permalinkinvokeSrcNameMap
invokeSrcNameMap
is also a map of services, but the attached type union is the identifier of the transitions the service can lead to, especially when a promise service terminates.
permalinkinternalEvents
internalEvents
holds the internal events triggered automatically by XState. When a promise service terminates or throws, internal events will be triggered by XState. Delayed transitions also trigger internal events to notify the state machine when a timer ends. These events will be listed in internalEvents
.
As stated by the comment in the __tip
property, the output of promise services can be explicitly typed in machines to make these internal events more powerful:
ts
// appMachine.tsconst appMachine = createMachine({tsTypes: {} as import("./appMachine.typegen").Typegen0,schema: {services: {} as {submitDataToServer: {data: { status: number };};},},// ...});
permalinkmissingImplementations
Finally, the Typegen generates a type for all the named actions, delays, guards, and services referenced but not implemented: missingImplementations
.
To reference them, we can pass an option object as the second parameter of the createMachine
function:
ts
// appMachine.tsconst appMachine = createMachine({tsTypes: {} as import("./appMachine.typegen").Typegen0,// ...},{actions: {assignServerResponseToContext: assign({}),},services: {submitDataToServer: async () => {},},});
If you don’t configure the named actions, delays, guards, and services in your machine, XState will force you to by erroring when using the machine at the end, either with the interpret
function or from the useMachine
hook for React library.
You will have to provide the definitions before being able to use the machine. You can use the .withConfig()
function to augment the configuration of the state machine and provide the missing definitions:
ts
import { interpret } from "xstate";interpret(appMachine.withConfig({actions: {assignServerResponseToContext: assign({}),},services: {submitDataToServer: async () => {},},})).start();
permalinkFavor naming instead of inlining code
For the Typegen to work, you must name your actions, delays, guards, and services instead of writing their implementation inline:
ts
// appMachine.tsconst appMachine = createMachine({states: {submitting: {invoke: {// Does not work well with the Typegen ❌src: async (context, event) => { /** */ },// Perfect! ✅src: "submitDataToServer",},},},});
It also makes working with Stately Studio easier, so go for it!
permalinkHide generated files from the File Tree in VS Code
After installing Stately’s VS Code extension, you will be asked whether you want to hide generated files from the File Tree View in VS Code. It will hide the file by default and allow you to unfold the source file as a directory.
These autogenerated files should not be committed to your Git repository. Instead, you should gitignore them and generate them when needed. During development, let the VS Code extension generate Typegen files for you. Use XState’s CLI to generate them for production builds.
permalinkWrap up
XState support of TypeScript has gotten so much better with the Typegen. It’s a tool you should use!
I hope you were able to find my explanation useful to you.
In the next post, you will learn how to invoke and spawn actors!
Best,
Baptiste