# 使用 TypeScript

¥Using TypeScript

由于 XState 是用 TypeScript (opens new window) 编写的,因此对状态图进行强类型化是有用且值得鼓励的。

¥As XState is written in TypeScript (opens new window), strongly typing your statecharts is useful and encouraged.

import { createMachine } from 'xstate';

const lightMachine = createMachine({
  schema: {
    // The context (extended state) of the machine
    context: {} as { elapsed: number },
    // The events this machine handles
    events: {} as
      | { type: 'TIMER' }
      | { type: 'POWER_OUTAGE' }
      | { type: 'PED_COUNTDOWN'; duration: number }
  }
  /* Other config... */
});

schema 属性提供上下文和事件具有许多优点:

¥Providing the context and events to the schema attribute gives many advantages:

  • 上下文类型/接口 (TContext) 被传递到操作、防护、服务等。它还会传递到深层嵌套状态。

    ¥The context type/interface (TContext) is passed on to actions, guards, services and more. It is also passed to deeply nested states.

  • 事件类型 (TEvent) 确保在转换配置中仅使用指定的事件(以及特定于内置 XState 的事件)。提供的事件对象形状也会传递给操作、防护和服务。

    ¥The event type (TEvent) ensures that only specified events (and built-in XState-specific ones) are used in transition configs. The provided event object shapes are also passed on to actions, guards, and services.

  • 你发送到机器的事件将是强类型的,使你对将接收的有效负载形状更有信心。

    ¥Events which you send to the machine will be strongly typed, offering you much more confidence in the payload shapes you'll be receiving.

# Typegen <徽章文本="4.29+" />

¥Typegen 4.29+

实验特性

此功能处于测试阶段!请参阅下面有关已知限制的部分,了解我们正在积极寻求改进的内容。

¥This feature is in beta! See the section on known limitations below to see what we're actively looking to improve.

使用我们的 VS Code 扩展 (opens new window)CLI,你可以自动为 XState 生成智能类型。

¥Using our VS Code extension (opens new window) or our CLI, you can automatically generate intelligent typings for XState.

你可以通过以下方式开始:

¥Here's how you can get started:

  1. 下载并安装 VS Code 扩展 (opens new window) 或安装 CLI 并运行带有 --watch 标志的 xstate typegen 命令。

    ¥Download and install the VS Code extension (opens new window) OR install the CLI and run the xstate typegen command with the --watch flag.

  2. 打开一个新文件并创建一台新机器,传递架构属性:

    ¥Open a new file and create a new machine, passing the schema attributes:

import { createMachine } from 'xstate';

const machine = createMachine({
  schema: {
    context: {} as { value: string },
    events: {} as { type: 'FOO'; value: string } | { type: 'BAR' }
  },
  initial: 'a',
  context: {
    value: ''
  },
  states: {
    a: {
      on: {
        FOO: {
          actions: 'consoleLogValue',
          target: 'b'
        }
      }
    },
    b: {
      entry: 'consoleLogValueAgain'
    }
  }
});
  1. tsTypes: {} 添加到机器中并保存文件:

    ¥Add tsTypes: {} to the machine and save the file:

const machine = createMachine({
  tsTypes: {},
  schema: {
    context: {} as { value: string },
    events: {} as { type: 'FOO'; value: string } | { type: 'BAR' }
  },
  context: {
    value: ''
  },
  initial: 'a',
  states: {
    /* ... */
  }
});
  1. 扩展应该自动向机器添加一个通用的:

    ¥The extension should automatically add a generic to the machine:

const machine = createMachine({
  tsTypes: {} as import('./filename.typegen').Typegen0
  /* ... */
});
  1. 将第二个参数添加到 createMachine 调用中 - 这是你为机器实现操作、服务、防护和延迟的地方。

    ¥Add a second parameter into the createMachine call - this is where you implement the actions, services, guards and delays for the machine.

const machine = createMachine(
  {
    /* ... */
  },
  {
    actions: {
      consoleLogValue: (context, event) => {
        // Wow! event is typed to { type: 'FOO' }
        console.log(event.value);
      },
      consoleLogValueAgain: (context, event) => {
        // Wow! event is typed to { type: 'FOO' }
        console.log(event.value);
      }
    }
  }
);

你会注意到选项中的事件对于导致触发操作的事件是强类型的。对于行动、守卫、服务和延误都是如此。

¥You'll notice that the events in the options are strongly typed to the events that cause the action to be triggered. This is true for actions, guards, services and delays.

你还会注意到 state.matchestags 和机器的其他部分现在是类型安全的。

¥You'll also notice that state.matches, tags and other parts of the machine are now type-safe.

# 打字 Promise 服务

¥Typing promise services

你可以使用生成的类型来指定基于 Promise 的服务的返回类型,方法是使用 services 架构属性:

¥You can use the generated types to specify the return type of promise-based services, by using the services schema property:

import { createMachine } from 'xstate';

createMachine(
  {
    schema: {
      services: {} as {
        myService: {
          // The data that gets returned from the service
          data: { id: string };
        };
      }
    },
    invoke: {
      src: 'myService',
      onDone: {
        actions: 'consoleLogId'
      }
    }
  },
  {
    services: {
      myService: async () => {
        // This return type is now type-safe
        return {
          id: '1'
        };
      }
    },
    actions: {
      consoleLogId: (context, event) => {
        // This event type is now type-safe
        console.log(event.data.id);
      }
    }
  }
);

# 如何充分利用 VS Code 扩展

¥How to get the most out of the VS Code extension

# 使用命名的操作/守卫/服务

¥Use named actions/guards/services

我们对这种方法的建议是主要使用命名的操作/防护/服务,而不是内联的。

¥Our recommendation with this approach is to mostly use named actions/guards/services, not inline ones.

这是最佳的:

¥This is optimal:

createMachine(
  {
    entry: ['sayHello']
  },
  {
    actions: {
      sayHello: () => {
        console.log('Hello!');
      }
    }
  }
);

这很有用,但不太理想:

¥This is useful, but less optimal:

createMachine({
  entry: [
    () => {
      console.log('Hello!');
    }
  ]
});

命名操作/服务/防护允许:

¥Named actions/services/guards allow for:

  • 更好的可视化,因为名称出现在状态图中

    ¥Better visualisation, because the names appear in the statechart

  • 更容易理解的代码

    ¥Easier-to-understand code

  • useMachinemachine.withConfig 中的覆盖

    ¥Overrides in useMachine, or machine.withConfig

# 生成的文件

¥The generated files

我们建议你 gitignore 存储库中生成的文件 (*filename*.typegen.ts)。

¥We recommend you gitignore the generated files (*filename*.typegen.ts) from the repository.

你可以使用 CLI 在 CI 上重新生成它们,例如通过安装后脚本:

¥You can use the CLI to regenerate them on CI, for instance via a postinstall script:

{
  "scripts": {
    "postinstall": "xstate typegen \"./src/**/*.ts?(x)\""
  }
}

# 不要使用枚举

¥Don't use enums

枚举是 XState TypeScript 中使用的常见模式。它们经常被用来声明状态名。像这样:

¥Enums were a common pattern used with XState TypeScript. They were often used to declare state names. like this:

enum States {
  A,
  B
}

createMachine({
  initial: States.A,
  states: {
    [States.A]: {},
    [States.B]: {}
  }
});

然后你可以在生成的机器上检查 state.matches(States.A)。这允许对状态名称进行类型安全检查。

¥You can then check state.matches(States.A) on the resulting machine. This allows for type-safe checks of state names.

使用 typegen,不再需要使用枚举 - 所有 state.matches 类型都是类型安全的。我们的静态分析工具目前不支持枚举。我们也不太可能用 typegen 支持它们,因为它们增加了复杂性,但收益却相对较小。

¥With typegen, using enums is no longer necessary - all state.matches types are type-safe. Enums are currently not supported by our static analysis tool. It's also unlikely that we'll ever support them with typegen due to the complexity they add for comparatively little gain.

使用 typegen 代替枚举,并依赖它提供的类型安全强度。

¥Instead of enums, use typegen and rely on the strength of the type-safety it provides.

# 已知的限制

¥Known limitations

# 始终转换/引发事件

¥Always transitions/raised events

如果操作/服务/防护/延迟被称为 "作为回应" 来始终转换或引发事件,则它们当前可能会被错误注释。我们正在努力解决这个问题,无论是在 XState 还是在 typegen 中。

¥Actions/services/guards/delays might currently get incorrectly annotated if they are called "in response" to always transitions or raised events. We are working on fixing this, both in XState and in the typegen.

# 配置对象

¥Config Objects

MachineConfig<TContext, any, TEvent> 的泛型类型与 createMachine<TContext, TEvent> 的泛型类型相同。当你在 createMachine(...) 函数之外定义机器配置对象时,这很有用,并有助于防止 推断错误 (opens new window)

¥The generic types for MachineConfig<TContext, any, TEvent> are the same as those for createMachine<TContext, TEvent>. This is useful when you are defining a machine config object outside of the createMachine(...) function, and helps prevent inference errors (opens new window):

import { MachineConfig } from 'xstate';

const myMachineConfig: MachineConfig<TContext, any, TEvent> = {
  id: 'controller',
  initial: 'stopped',
  states: {
    stopped: {
      /* ... */
    },
    started: {
      /* ... */
    }
  }
  // ...
};

# 类型状态 4.7+

¥Typestates 4.7+

类型状态是一个基于状态 value 缩小整体状态 context 形状的概念。这有助于防止不可能的状态并缩小 context 在给定状态下的范围,而无需编写过多的断言。

¥Typestates are a concept that narrow down the shape of the overall state context based on the state value. This can be helpful in preventing impossible states and narrowing down what the context should be in a given state, without having to write excessive assertions.

Typestate 是一个由两个属性组成的接口:

¥A Typestate is an interface consisting of two properties:

  • value - typestate 的状态值(复合状态应使用对象语法引用;例如,{ idle: 'error' } 而不是 "idle.error"

    ¥value - the state value of the typestate (compound states should be referenced using object syntax; e.g., { idle: 'error' } instead of "idle.error")

  • context - 当状态与给定的 value 匹配时,类型状态的缩小上下文

    ¥context - the narrowed context of the typestate when the state matches the given value

机器的类型状态被指定为 createMachine<TContext, TEvent, TTypestate> 中的第三个通用类型。

¥The typestates of a machine are specified as the 3rd generic type in createMachine<TContext, TEvent, TTypestate>.

示例:

¥Example:

import { createMachine, interpret } from 'xstate';

interface User {
  name: string;
}

interface UserContext {
  user?: User;
  error?: string;
}

type UserEvent =
  | { type: 'FETCH'; id: string }
  | { type: 'RESOLVE'; user: User }
  | { type: 'REJECT'; error: string };

type UserTypestate =
  | {
      value: 'idle';
      context: UserContext & {
        user: undefined;
        error: undefined;
      };
    }
  | {
      value: 'loading';
      context: UserContext;
    }
  | {
      value: 'success';
      context: UserContext & { user: User; error: undefined };
    }
  | {
      value: 'failure';
      context: UserContext & { user: undefined; error: string };
    };

const userMachine = createMachine<UserContext, UserEvent, UserTypestate>({
  id: 'user',
  initial: 'idle',
  states: {
    idle: {
      /* ... */
    },
    loading: {
      /* ... */
    },
    success: {
      /* ... */
    },
    failure: {
      /* ... */
    }
  }
});

const userService = interpret(userMachine);

userService.subscribe((state) => {
  if (state.matches('success')) {
    // from the UserState typestate, `user` will be defined
    state.context.user.name;
  }
});

警告

复合状态应该对所有父状态值进行显式建模,以避免测试子状态时出现类型错误。

¥Compound states should have all parent state values explicitly modelled to avoid type errors when testing substates.

type State =
  /* ... */
  | {
      value: 'parent';
      context: Context;
    }
  | {
      value: { parent: 'child' };
      context: Context;
    };
/* ... */

如果两个状态具有相同的上下文类型,则可以通过使用值的类型联合来合并它们的声明。

¥Where two states have identical context types, their declarations can be merged by using a type union for the value.

type State =
  /* ... */
  {
    value: 'parent' | { parent: 'child' };
    context: Context;
  };
/* ... */

# 故障排除

¥Troubleshooting

XState 和 TypeScript 有一些已知的限制。我们热爱 TypeScript,并且不断努力使其在 XState 中获得更好的体验。

¥There are some known limitations with XState and TypeScript. We love TypeScript, and we're constantly pressing ahead to make it a better experience in XState.

以下是一些已知问题,所有这些问题都可以解决:

¥Here are some known issues, all of which can be worked around:

# 机器选项中的事件

¥Events in machine options

当你使用 createMachine 时,你可以将实现传递给配置中的命名操作/服务/防护。例如:

¥When you use createMachine, you can pass in implementations to named actions/services/guards in your config. For instance:

import { createMachine } from 'xstate';

interface Context {}

type Event =
  | { type: 'EVENT_WITH_FLAG'; flag: boolean }
  | {
      type: 'EVENT_WITHOUT_FLAG';
    };

createMachine(
  {
    schema: {
      context: {} as Context,
      events: {} as Event
    },
    on: {
      EVENT_WITH_FLAG: {
        actions: 'consoleLogData'
      }
    }
  },
  {
    actions: {
      consoleLogData: (context, event) => {
        // This will error at .flag
        console.log(event.flag);
      }
    }
  }
);

这个错误的原因是因为在 consoleLogData 函数内部,我们不知道哪个事件导致它触发。管理此问题的最简洁方法是自己断言事件类型。

¥The reason this errors is because inside the consoleLogData function, we don't know which event caused it to fire. The cleanest way to manage this is to assert the event type yourself.

createMachine(config, {
  actions: {
    consoleLogData: (context, event) => {
      if (event.type !== 'EVENT_WITH_FLAG') return
      // No more error at .flag!
      console.log(event.flag);
    };
  }
})

有时也可以内联移动实现。

¥It's also sometimes possible to move the implementation inline.

import { createMachine } from 'xstate';

createMachine({
  schema: {
    context: {} as Context,
    events: {} as Event
  },
  on: {
    EVENT_WITH_FLAG: {
      actions: (context, event) => {
        // No more error, because we know which event
        // is responsible for calling this action
        console.log(event.flag);
      }
    }
  }
});

这种方法并不适用于所有情况。该动作失去了名称,因此在可视化工具中看起来不太美观。这还意味着,如果该操作在多个位置重复,你需要将其复制粘贴到所有需要的位置。

¥This approach doesn't work for all cases. The action loses its name, so it becomes less nice to look at in the visualiser. It also means if the action is duplicated in several places you'll need to copy-paste it to all the places it's needed.

# 条目操作中的事件类型

¥Event types in entry actions

内联输入操作中的事件类型当前未键入导致它们的事件。考虑这个例子:

¥Event types in inline entry actions are not currently typed to the event that led to them. Consider this example:

import { createMachine } from 'xstate';

interface Context {}

type Event =
  | { type: 'EVENT_WITH_FLAG'; flag: boolean }
  | {
      type: 'EVENT_WITHOUT_FLAG';
    };

createMachine({
  schema: {
    context: {} as Context,
    events: {} as Event
  },
  initial: 'state1',
  states: {
    state1: {
      on: {
        EVENT_WITH_FLAG: {
          target: 'state2'
        }
      }
    },
    state2: {
      entry: [
        (context, event) => {
          // This will error at .flag
          console.log(event.flag);
        }
      ]
    }
  }
});

在这里,我们不知道什么事件导致了 entrystate2 的操作。解决这个问题的唯一方法是执行与上面类似的技巧:

¥Here, we don't know what event led to the entry action on state2. The only way to fix this is to do a similar trick to above:

entry: [
  (context, event) => {
    if (event.type !== 'EVENT_WITH_FLAG') return;
    // No more error at .flag!
    console.log(event.flag);
  }
];

# 分配行为异常的操作

¥Assign action behaving strangely

当在 strict: true 模式下运行时,分配操作有时会表现得非常奇怪。

¥When run in strict: true mode, assign actions can sometimes behave very strangely.

interface Context {
  something: boolean;
}

createMachine({
  schema: {
    context: {} as Context
  },
  context: {
    something: true
  },
  entry: [
    // Type 'AssignAction<{ something: false; }, AnyEventObject>' is not assignable to type 'string'.
    assign(() => {
      return {
        something: false
      };
    }),
    // Type 'AssignAction<{ something: false; }, AnyEventObject>' is not assignable to type 'string'.
    assign({
      something: false
    }),
    // Type 'AssignAction<{ something: false; }, AnyEventObject>' is not assignable to type 'string'.
    assign({
      something: () => false
    })
  ]
});

你尝试的任何方法可能都不起作用 - 所有语法都有错误。该修复非常奇怪,但始终有效。将未使用的 context 参数添加到分配器函数的第一个参数中。

¥It might appear that nothing you try works - all syntaxes are buggy. The fix is very strange, but works consistently. Add an unused context argument to the first argument of your assigner function.

entry: [
  // No more error!
  assign((context) => {
    return {
      something: false,
    };
  }),
  // No more error!
  assign({
    something: (context) => false,
  }),
  // Unfortunately this technique doesn't work for this syntax
  // assign({
  //   something: false
  // }),
],

这是一个需要修复的严重错误,需要将我们的代码库移至严格模式,但我们计划在 V5 中进行此操作。

¥This is a nasty bug to fix and involves moving our codebase to strict mode, but we're planning to do it in V5.

# keyofStringsOnly

如果你看到此错误:

¥If you are seeing this error:


Type error: Type 'string | number' does not satisfy the constraint 'string'.
Type 'number' is not assignable to type 'string'. TS2344

确保你的 tsconfig 文件不包含 "keyofStringsOnly": true,

¥Ensure that your tsconfig file does not include "keyofStringsOnly": true,.