TypeScript в effector
Effector предоставляет первоклассную поддержку TypeScript из коробки, что дает вам надежную типизацию и отличный опыт разработки при работе с библиотекой. В этом разделе мы рассмотрим как базовые концепции типизации, так и продвинутые техники работы с типами в effector.
Типизация событий
События в effector могут быть типизированы при помощи передачи типа в дженерик функции, однако если не передавать ничего, то в таком случае событие будет с типом EventCallable<void>
:
import { createEvent } from "effector";
// Событие без параметровconst clicked = createEvent();// EventCallable<void>
// Событие с параметромconst userNameChanged = createEvent<string>();// EventCallable<string>
// Событие со сложным параметромconst formSubmitted = createEvent<{ username: string; password: string;}>();// EventCallable<{ username: string;password: string; }>
Типы событий
В effector для событий может быть несколько типов, где T
- тип хранимого значения:
EventCallable<T>
- событие, которое может вызвать.Event<T>
- производное событие, которое нельзя вызвать в ручную.
Типизация методов событий
event.prepend
Чтобы добавить типы к событиям, созданным с помощью event.prepend, необходимо добавить тип либо в аргумент функции prepend
, либо как дженерик
const message = createEvent<string>();
const userMessage = message.prepend((text: string) => text);// userMessage имеет тип EventCallable<string>
const warningMessage = message.prepend<string>((warnMessage) => warnMessage);// warningMessage имеет тип EventCallable<string>
Типизация сторов
Сторы также можно типизировать при помощи передачи типа в дженерик функции, либо указав дефолтное значение при инициализации, тогда ts будет выводить тип из этого значения:
import { createStore } from "effector";
// Базовый стор с примитивным значением// StoreWritable<number>const $counter = createStore(0);
// Стор со сложным объектным типомinterface User { id: number; name: string; role: "admin" | "user";}
// StoreWritable<User>const $user = createStore<User>({ id: 1, name: "Bob", role: "user",});
// Store<string>const $userNameAndRole = $user.map((user) => `User name and role: ${user.name} and ${user.role}`);
Типы сторов
В эффектор существуют два типа сторов, где T
- тип хранимого значения:
Store<T>
- тип производного стора, в который нельзя записать новые данные.StoreWritable<T>
- тип стора, в который можно записывать новые данные при помощиon
илиsample
.
Типизация эффектов
При обычном использовании TypeScript будет выводить типы в зависимости от возвращаемого результата функции, а также ее аргументов.
Однако, createEffect
поддерживает типизацию входных параметров, возвращаемого результата и ошибок через дженерик:
import { createEffect } from "effector";
// Базовый эффект// Effect<string, User, Error>const fetchUserFx = createEffect(async (userId: string) => { const response = await fetch(`/api/users/${userId}`); const result = await response.json();
return result as User;});
import { createEffect } from "effector";
// Базовый эффект// Effect<string, User, Error>const fetchUserFx = createEffect<string, User>(async (userId) => { const response = await fetch(`/api/users/${userId}`); const result = await response.json();
return result;});
Типизация функции обработчика вне эффекта
В случае, если функция обработчик определен вне эффекта, то для типизации вам нужно будет передать тип этой функции:
const sendMessage = async (params: { text: string }) => { // ... return "ok";};
const sendMessageFx = createEffect<typeof sendMessage, AxiosError>(sendMessage);// => Effect<{text: string}, string, AxiosError>
Кастомные ошибки эффекта
Некоторый код может выдать исключения только некоторых типов. В эффектах для описания типов ошибок используется третий дженерик Fail
.
// Определяем типы ошибок APIinterface ApiError { code: number; message: string;}
// Создаём типизированный эффектconst fetchUserFx = createEffect<string, User, ApiError>(async (userId) => { const response = await fetch(`/api/users/${userId}`);
if (!response.ok) { throw { code: response.status, message: "Failed to fetch user", } as ApiError; }
return response.json();});
Типизация методов
sample
Типизация filter
Если вам необходимо получить конкретный тип, то для этого вам нужно в ручную указать ожидаемый тип, сделать это можно при помощи типов придикатов:
type UserMessage = { kind: "user"; text: string };type WarnMessage = { kind: "warn"; warn: string };
const message = createEvent<UserMessage | WarnMessage>();const userMessage = createEvent<UserMessage>();
sample({ clock: message, filter: (msg): msg is UserMessage => msg.kind === "user", target: userMessage,});
Если вам нужно произвести проверку в filter
на существование данных, то вы можете просто передать Boolean
:
import { createEvent, createStore, sample } from "effector";
interface User { id: string; name: string; email: string;}
// Событияconst formSubmitted = createEvent();const userDataSaved = createEvent<User>();
// Состоянияconst $currentUser = createStore<User | null>(null);
// При сабмите формы отправляем данные только если юзер существуетsample({ clock: formSubmitted, source: $currentUser, filter: Boolean, // отфильтровываем null target: userDataSaved,});
// Теперь userDataSaved получит только существующие данные пользователя
Типизация filter
и fn
Как упоминалось выше, если использовать предикаты типов в filter
, то все отработает корректно и в target
попадет нужный тип.
Однако, такая механика не отработает как нужно при использовании filter
и fn
вместе. В таком случае вам потребуется в ручную указать тип данных параметров filter
, а также добавить предикаты типов. Это происходит из-за того, что TypeScript не может корректно вывести тип в fn
после filter
, если тип не указан явно. Это ограничение системы типов TypeScript.
type UserMessage = { kind: "user"; text: string };type WarnMessage = { kind: "warn"; warn: string };type Message = UserMessage | WarnMessage;
const message = createEvent<Message>();const userText = createEvent<string>();
sample({ clock: message, filter: (msg: Message): msg is UserMessage => msg.kind === "user", fn: (msg) => msg.text, target: userText,});
// userMessage has type Event<string>
Начиная с TypeScript версии >= 5.5 вы можете не писать предикаты типов, а просто указать тип аргумента, а TypeScript сам поймет, что нужно вывести:
filter: (msg: Message) => msg.kind === "user"
,
attach
Чтобы позволить TypeScript выводить типы создаваемого эффекта, можно добавить тип к первому аргументу mapParams
, который станет дженериком Params
у результата:
const sendTextFx = createEffect<{ message: string }, "ok">(() => { // ...
return "ok";});
const sendWarningFx = attach({ effect: sendTextFx, mapParams: (warningMessage: string) => ({ message: warningMessage }),});// sendWarningFx имеет тип Effect<{message: string}, 'ok'>
split
Вы можете использовать предикаты типов для разделения исходного типа события на несколько вариантов:
type UserMessage = { kind: "user"; text: string };type WarnMessage = { kind: "warn"; warn: string };
const message = createEvent<UserMessage | WarnMessage>();
const { userMessage, warnMessage } = split(message, { userMessage: (msg): msg is UserMessage => msg.kind === "user", warnMessage: (msg): msg is WarnMessage => msg.kind === "warn",});// userMessage имеет тип Event<UserMessage>// warnMessage имеет тип Event<WarnMessage>
type UserMessage = { kind: "user"; text: string };type WarnMessage = { kind: "warn"; warn: string };
const message = createEvent<UserMessage | WarnMessage>();
const { userMessage, warnMessage } = split(message, { userMessage: (msg) => msg.kind === "user", warnMessage: (msg) => msg.kind === "warn",});// userMessage имеет тип Event<UserMessage>// warnMessage имеет тип Event<WarnMessage>
createApi
Чтобы позволить TypeScript выводить типы создаваемых событий, можно добавить тип ко второму аргументу обработчиков
const $count = createStore(0);
const { add, sub } = createApi($count, { add: (x, add: number) => x + add, sub: (x, sub: number) => x - sub,});
// add имеет тип Event<number>// sub имеет тип Event<number>
is
Методы группы is могут помочь вывести тип юнита, то есть они действуют как TypeScript type guards. Это применяется в написании типизированных утилит:
export function getUnitType(unit: unknown) { if (is.event(unit)) { // здесь юнит имеет тип Event<any> return "event"; } if (is.effect(unit)) { // здесь юнит имеет тип Effect<any, any> return "effect"; } if (is.store(unit)) { // здесь юнит имеет тип Store<any> return "store"; }}
merge
При объединении событий можно получить союз их типов:
import { createEvent, merge } from "effector";
const firstEvent = createEvent<string>();const secondEvent = createEvent<number>();
const merged = merge([firstEvent, secondEvent]);// Event<string | number>
// Можно также объединять события с одинаковыми типамиconst buttonClicked = createEvent<MouseEvent>();const linkClicked = createEvent<MouseEvent>();
const anyClick = merge([buttonClicked, linkClicked]);// Event<MouseEvent>
merge
принимает дженерик параметр, где можно указать какого типа событий он ожидает:
import { createEvent, merge } from "effector";
const firstEvent = createEvent<string>();const secondEvent = createEvent<number>();
const merged = merge<number>([firstEvent, secondEvent]);// ^// Type 'EventCallable<string>' is not assignable to type 'Unit<number>'.
Утилиты для типов
Effector предоставляет набор утилитных типов для работы с типами юнитов:
UnitValue
Тип UnitValue
служит для извлечение типа данных из юнитов:
import { UnitValue, createEffect, createStore, createEvent } from "effector";
const event = createEvent<{ id: string; name?: string } | { id: string }>();type UnitEventType = UnitValue<typeof event>;// {id: string; name?: string | undefined} | {id: string}
const $store = createStore([false, true]);type UnitStoreType = UnitValue<typeof $store>;// boolean[]
const effect = createEffect<{ token: string }, any, string>(() => {});type UnitEffectType = UnitValue<typeof effect>;// {token: string}
const scope = fork();type UnitScopeType = UnitValue<typeof scope>;// any
StoreValue
StoreValue
по своей сути похож на UnitValue
, но работает только со стором:
import { createStore, StoreValue } from "effector";
const $store = createStore(true);
type StoreValueType = StoreValue<typeof $store>;// boolean
EventPayload
Извлекает тип данных из событий.
Похож на UnitValue
, но только для событий
import { createEvent, EventPayload } from "effector";
const event = createEvent<{ id: string }>();
type EventPayloadType = EventPayload<typeof event>;// {id: string}
EffectParams
Принимает тип эффекта в параметры дженерика, позволяет получить тип параметров эффекта.
import { createEffect, EffectParams } from "effector";
const fx = createEffect< { id: string }, { name: string; isAdmin: boolean }, { statusText: string; status: number }>(() => { // ... return { name: "Alice", isAdmin: false };});
type EffectParamsType = EffectParams<typeof fx>;// {id: string}
EffectResult
Принимает тип эффекта в параметры дженерика, позволяет получить тип возвращаемого значения эффекта.
import { createEffect, EffectResult } from "effector";
const fx = createEffect< { id: string }, { name: string; isAdmin: boolean }, { statusText: string; status: number }>(() => ({ name: "Alice", isAdmin: false }));
type EffectResultType = EffectResult<typeof fx>;// {name: string; isAdmin: boolean}
EffectError
Принимает тип эффекта в параметры дженерика, позволяет получить тип ошибки эффекта.
import { createEffect, EffectError } from "effector";
const fx = createEffect< { id: string }, { name: string; isAdmin: boolean }, { statusText: string; status: number }>(() => ({ name: "Alice", isAdmin: false }));
type EffectErrorType = EffectError<typeof fx>;// {statusText: string; status: number}
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.