# 使用 TypeScript

由于 XState 是用 TypeScript (opens new window) 编写的,也建议你在编写状态图的时候使用它。 下面是一个轻型状态机示例:

// 状态机处理的事件
type LightEvent =
  | { type: 'TIMER' }
  | { type: 'POWER_OUTAGE' }
  | { type: 'PED_COUNTDOWN'; duration: number };

// 状态机的上下文(扩展状态)
interface LightContext {
  elapsed: number;
}

const lightMachine = createMachine<LightContext, LightEvent>({
  key: 'light',
  initial: 'green',
  context: { elapsed: 0 },
  states: {
    green: {
      on: {
        TIMER: { target: 'yellow' },
        POWER_OUTAGE: { target: 'red' }
      }
    },
    yellow: {
      on: {
        TIMER: { target: 'red' },
        POWER_OUTAGE: { target: 'red' }
      }
    },
    red: {
      on: {
        TIMER: { target: 'green' },
        POWER_OUTAGE: { target: 'red' }
      },
      initial: 'walk',
      states: {
        walk: {
          on: {
            PED_COUNTDOWN: { target: 'wait' }
          }
        },
        wait: {
          on: {
            PED_COUNTDOWN: {
              target: 'stop',
              cond: (context, event) => {
                return event.duration === 0 && context.elapsed > 0;
              }
            }
          }
        },
        stop: {
          // 瞬态过渡
          always: {
            target: '#light.green'
          }
        }
      }
    }
  }
});

提供上下文和事件作为 createMachine() 函数的通用参数有很多优点:

  • 上下文类型/接口(TContext)被传递给动作、守卫、服务等。 它也被传递到深度嵌套的状态。
  • 事件类型 (TEvent) 确保在转换配置中只使用指定的事件(和内置的 XState 特定事件)。 提供的事件对象形状也传递给动作、守卫和服务。
  • 你发送到状态机的事件将是强类型的,让你对接收的有效数据形态更有信心。

# 配置对象

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

import { MachineConfig } from 'xstate';

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

# 类型状态 4.7+

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

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

  • value - 类型状态的状态值(复合状态应该使用对象语法引用;例如,{ idle: 'error' } 而不是 "idle.error"
  • context - 当状态与给定的 value 匹配时,类型状态的缩小上下文

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

示例:

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')) {
    // 从 UserState 类型状态,将定义 `user`
    state.context.user.name;
  }
});

注意

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

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

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

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

# 故障排除

XState 和 TypeScript 存在一些已知的限制。 我们喜欢 TypeScript,我们不断地努力使其在 XState 中获得更好的体验。

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

# 状态机选项中的事件

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

interface Context {}

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

createMachine<Context, Event>(
  {
    on: {
      EVENT_WITH_FLAG: {
        actions: 'consoleLogData'
      }
    }
  },
  {
    actions: {
      consoleLogData: (context, event) => {
        // 这将在 .flag 处出错
        console.log(event.flag);
      }
    }
  }
);

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

createMachine<Context, Event>(machine, {
  actions: {
    consoleLogData: (context, event) => {
      if (event.type !== 'EVENT_WITH_FLAG') return
      // .flag 不再有错误!
      console.log(event.flag);
    };
  }
})

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

createMachine<Context, Event>({
  on: {
    EVENT_WITH_FLAG: {
      actions: (context, event) => {
        // 不再出错,因为我们知道哪个事件负责调用这个动作
        console.log(event.flag);
      }
    }
  }
});

这种方法并不适用于所有情况。 动作失去了它的名字,所以在可视化器中看起来不太好。 这也意味着如果操作在多个位置重复,你需要将其复制粘贴到所有需要的位置。

# 进入动作中的事件类型

内联输入动作中的事件。 考虑这个例子:

interface Context {}

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

createMachine<Context, Event>({
  initial: 'state1',
  states: {
    state1: {
      on: {
        EVENT_WITH_FLAG: {
          target: 'state2'
        }
      }
    },
    state2: {
      entry: [
        (context, event) => {
          // 这将在 .flag 处出错
          console.log(event.flag);
        }
      ]
    }
  }
});

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

entry: [
  (context, event) => {
    if (event.type !== 'EVENT_WITH_FLAG') return;
    // .flag 不再有错误!
    console.log(event.flag);
  }
];

# 状态机选项中的 onDone/onError 事件

基于 Promise 的服务的结果很难在 XState 中安全地输入。 例如,这样的状态机:

interface Data {
  flag: boolean;
}

interface Context {}

type Event = {
  // 在此处添加以显示 TS 错误
  type: 'UNUSED_EVENT';
};

createMachine<Context, Event>(
  {
    invoke: {
      src: async () => {
        const data: Data = {
          flag: true
        };
        return data;
      },
      onDone: {
        actions: 'consoleLogData'
      },
      onError: {
        actions: 'consoleLogError'
      }
    }
  },
  {
    actions: {
      consoleLogData: (context, event) => {
        // 此行出错 - 数据不存在!
        console.log(event.data.flag);
      },
      consoleLogError: (context, event) => {
        // 此行出错 - 数据不存在!
        console.log(event.data);
      }
    }
  }
);

令人沮丧的是,解决此问题的最佳方法是将 event 转换为 any 并根据我们知道的内容重新分配它:

import { DoneInvokeEvent, ErrorPlatformEvent } from 'xstate'

actions: {
  consoleLogData: (context, _event: any) => {
    const event: DoneInvokeEvent<Data> = _event;
    console.log(event.data.flag);
  },
  consoleLogError: (context, _event: any) => {
    const event: ErrorPlatformEvent = _event;
    // Event.data 通常是 `Error` 类型
    console.log(event.data.message);
  }
}

# 分配行为异常

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

interface Context {
  something: boolean;
}

createMachine<Context>({
  context: {
    something: true
  },
  entry: [
    // 输入 'AssignAction<{ something: false; }, AnyEventObject>' 不可分配给类型 'string'。
    assign(() => {
      return {
        something: false
      };
    }),
    // 输入 'AssignAction<{ something: false; }, AnyEventObject>' 不可分配给类型 'string'。
    assign({
      something: false
    }),
    // 输入 'AssignAction<{ something: false; }, AnyEventObject>' 不可分配给类型 'string'。
    assign({
      something: () => false
    })
  ]
});

看起来你尝试的任何方法都不起作用,所有语法都有问题。 修复很奇怪,但始终如一。 将未使用的 context 参数添加到 assign 函数的第一个参数。

entry: [
  // 没有更多的错误!
  assign((context) => {
    return {
      something: false,
    };
  }),
  // 没有更多的错误!
  assign({
    something: (context) => false,
  }),
  // 不幸的是,此技术不适用于此语法
  // assign({
  //   something: false
  // }),
],

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

# keyofStringsOnly

如果你看到此错误:


输入错误:输入'string | number' 不满足约束 'string'。
'number' 类型不能分配给 'string' 类型。 TS2344

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