Работа с TypeScript
TypeScript проверяет события, payload-ы, состояния и зависимости effects на этапе компиляции. В этом разделе показано, как настроить типизированные фабрики и React-хуки для приложения на lite-fsm.
Структура проекта
Ниже представлена структура файлов, которую мы будем использовать в примерах:
src/
├── App.tsx # Корневой компонент React-приложения
└── store/ # Директория для управления состоянием
├── index.ts # Экспорт store и типов
├── types.ts # Общие типы приложения
├── create-machine.ts # Типизированные версии API функций
├── hooks.ts # Типизированные React-хуки
└── machines/ # Директория с автоматами
├── counter.ts # Автомат для счетчикаПредварительная типизация функций API
Обычный паттерн для TypeScript-проектов — создать локальные типизированные версии функций API (createMachine, createEffect и т.д.). Они фиксируют union событий и тип зависимостей приложения, чтобы не повторять generics в каждой машине.
Шаг 1: Определение базовых типов событий и зависимостей
Сначала определите базовые типы для вашего приложения:
import type { AppState } from ".";
import type * as counter from "./machines/counter";
// Объединяем все типы событий из всех автоматов
export type AppEvents = counter.Events;
// Определяем тип зависимостей, которые будут доступны в эффектах
export type Dependencies = {
logger: {
log: (message: string) => void;
};
getState: () => AppState;
};Шаг 2: Создание типизированных версий функций API
Создайте файл, который будет реэкспортировать функции lite-fsm с предварительно примененными типами:
import type {
TypedCreateConfigFn,
TypedCreateEffectFn,
TypedCreateMachineFn,
TypedCreateReducerFn,
} from "@lite-fsm/core";
import {
createConfig as _createConfig,
createEffect as _createEffect,
createMachine as _createMachine,
createReducer as _createReducer,
} from "@lite-fsm/core";
import type { AppEvents, Dependencies } from "./types";
// Типизированные версии функций для приложения
export const createMachine: TypedCreateMachineFn<AppEvents, Dependencies> = _createMachine;
export const createReducer: TypedCreateReducerFn<AppEvents> = _createReducer;
export const createConfig: TypedCreateConfigFn<AppEvents> = _createConfig;
export const createEffect: TypedCreateEffectFn<AppEvents, Dependencies> = _createEffect;Шаг 3: Типизированные хуки для работы с React
Аналогичным образом, создайте типизированные версии React-хуков:
import type { TypedUseManagerHook, TypedUseSelectorHook, TypedUseTransitionHook } from "@lite-fsm/react";
import {
useManager as _useManager,
useSelector as _useSelector,
useTransition as _useTransition,
} from "@lite-fsm/react";
import type { FSMConfigType } from ".";
import type { AppEvents } from "./types";
// Типизированные версии хуков.
// TypedUseSelectorHook принимает store config (FSMConfigType), не computed AppState —
// `MachinesState<S>` будет вычислен автоматически.
export const useTransition: TypedUseTransitionHook<AppEvents> = _useTransition;
export const useSelector: TypedUseSelectorHook<FSMConfigType> = _useSelector;
export const useManager: TypedUseManagerHook<FSMConfigType, AppEvents> = _useManager;Шаг 4: Создание store
Теперь нужно объединить все автоматы в единый store и экспортировать типы:
import { MachineManager, type MachinesState } from "@lite-fsm/core";
import { devToolsMiddleware } from "@lite-fsm/middleware/devTools";
import { immerMiddleware } from "@lite-fsm/middleware/immer";
import { counter } from "./machines/counter";
import type { AppEvents, Dependencies } from "./types";
const cfg = { counter };
export type FSMConfigType = typeof cfg;
export type AppState = MachinesState<FSMConfigType>;
export const makeStore = () => {
const manager = MachineManager<FSMConfigType, AppEvents>(cfg, {
middleware: [immerMiddleware, devToolsMiddleware({ blacklistActions: [] })],
onError: console.error,
});
manager.setDependencies({
getState: manager.getState,
logger: console,
});
return manager;
};
export type AppStore = ReturnType<typeof makeStore>;Шаг 5: Создадим базовый шаблон автомата counter
import type { FSMEvent } from "@lite-fsm/core";
import { createMachine } from "../create-machine";
export type Events = FSMEvent<"DO_INIT">;
export const counter = createMachine({
config: {
IDLE: {
DO_INIT: "READY",
},
READY: {},
},
initialState: "IDLE",
initialContext: {},
});Автоматический вывод типов событий и состояний
При использовании типизированных функций TypeScript выводит доступные состояния из конфигурации автомата. Это уменьшает дублирование между config и типами.
import { createConfig, createMachine } from "../create-machine";
const config = createConfig({
IDLE: {
INCREMENT: "COUNTING",
RESET: "IDLE",
},
COUNTING: {
INCREMENT: null,
DECREMENT: null,
RESET: "IDLE",
},
});
// Автоматически выводим тип состояний из конфигурации
export type State = keyof typeof config;
// => "IDLE" | "COUNTING"
export type Events =
| { type: "INCREMENT"; payload: { step: number } }
| { type: "DECREMENT"; payload: { step: number } }
| { type: "RESET" };
export const counter = createMachine({
config,
initialState: "IDLE",
initialContext: {
count: 0,
},
// ...
});Такой подход уменьшает дублирование: при изменении config выведенный union состояний обновится вместе с ним.
Использование FSMEvent для типизации событий
Библиотека lite-fsm предоставляет встроенный тип FSMEvent для описания событий с payload:
Reducer в следующем примере написан в стиле Immer и рассчитан на immerMiddleware, подключенный в makeStore выше. Если middleware не используется, reducer должен вернуть новый объект { state, context }.
import type { FSMEvent } from "@lite-fsm/core";
import { createConfig, createMachine } from "../create-machine";
const config = createConfig({
IDLE: {
INCREMENT: "COUNTING",
DECREMENT: "IDLE",
RESET: "IDLE",
SET_VALUE: "IDLE",
},
COUNTING: {
INCREMENT: null,
DECREMENT: "IDLE",
RESET: "IDLE",
},
});
// FSMEvent — короткая запись для action-типов с payload и без
export type Events =
| FSMEvent<"INCREMENT">
| FSMEvent<"DECREMENT">
| FSMEvent<"RESET">
| FSMEvent<"SET_VALUE", { value: number }>;
export const counter = createMachine({
config,
initialState: "IDLE",
initialContext: {
count: 0,
},
reducer: (state, action) => {
switch (action.type) {
case "INCREMENT":
state.context.count += 1;
return;
case "DECREMENT":
state.context.count = Math.max(0, state.context.count - 1);
return;
case "RESET":
state.context.count = 0;
return;
case "SET_VALUE":
// TypeScript знает, что action.payload содержит value: number
state.context.count = action.payload.value;
return;
}
},
});
// Примеры использования:
// OK: manager.transition({ type: "INCREMENT" });
// OK: manager.transition({ type: "SET_VALUE", payload: { value: 42 } });
// Ошибка TypeScript: manager.transition({ type: "SET_VALUE" }); // Отсутствует payload
// Ошибка TypeScript: manager.transition({ type: "INCREMENT", payload: { value: 5 } }); // Лишний payloadFSMEvent делает форму событий явной: событие без payload не принимает payload, а событие с payload требует поле payload нужной формы.
Использование типизированных хуков в компонентах
В компонентах React используйте типизированные хуки:
import "./styles.css";
import { useSelector, useTransition } from "./store/hooks";
export default function App() {
const transition = useTransition();
const counterState = useSelector((s) => s.counter.state);
const count = useSelector((s) => s.counter.context.count);
return (
<div className="App">
<h1>Counter state: {counterState}</h1>
<h1>Counter value: {count}</h1>
<button
onClick={() => {
transition({ type: "INCREMENT", payload: { step: 1 } });
}}
>
+1
</button>
<button
onClick={() => {
transition({ type: "DECREMENT", payload: { step: 1 } });
}}
disabled={!count}
>
-1
</button>
<button
onClick={() => {
transition({ type: "RESET" });
}}
>
Сбросить
</button>
</div>
);
}Преимущества типизации
Предварительная типизация функций API lite-fsm даёт следующие преимущества:
- Проверка типов во время компиляции — TypeScript подсветит несоответствие событий, payload-ов и состояний.
- Автодополнение в IDE — доступны подсказки по состояниям, событиям и контексту.
- Рефакторинг — при изменении типов TypeScript укажет места, которые нужно обновить.
- Документирование контракта — типы показывают форму событий, контекста и dependencies.
- Меньше runtime-ошибок — часть ошибок переходов и payload-ов ловится до запуска приложения.
Дополнительные рекомендации
- Разделяйте типы по машинам — определяйте
Context,StateиEventsрядом с конкретным автоматом. - Используйте union-типы для событий — так TypeScript сможет сужать action по
action.type. - Используйте вывод типов — выводите union состояний из
config, когда это возможно. - Включайте строгий режим TypeScript — используйте
"strict": trueвtsconfig.json.
Заключение
TypeScript с lite-fsm делает контракты машин явными. Локальные типизированные фабрики и хуки уменьшают повторение generics и дают единый способ проверять события, состояния, context shape и dependencies во всём приложении.