Bu sahifa hali tarjima qilinmagan

Tarjima qoshish uchun havola boyicha o'tib Pull Request oching (havolaga o'tish).

Standart til uchun tarkibni ko'rsatadi.

TypeScript in effector

Effector provides first-class TypeScript support out of the box, giving you reliable typing and excellent development experience when working with the library. In this section, we’ll look at both basic typing concepts and advanced techniques for working with types in effector.

Typing Events

Events in Effector can be typed by passing a type to the generic function. However, if nothing is passed, the event will have the type EventCallable<void>:

import { createEvent } from "effector";

// Event without parameters
const clicked = createEvent();
// EventCallable<void>

// Event with parameter
const userNameChanged = createEvent<string>();
// EventCallable<string>

// Event with complex parameter
const formSubmitted = createEvent<{
  username: string;
  password: string;
}>();
// EventCallable<{ username: string; password: string; }>

Event Types

In Effector, events can have several types, where T is the stored value type:

  • EventCallable<T> - an event that can be called.
  • Event<T> - a derived event that cannot be called manually.

Typing Event Methods

event.prepend

To add types to events created using event.prepend, you need to add the type either in the prepend function argument or as a generic:

const message = createEvent<string>();

const userMessage = message.prepend((text: string) => text);
// userMessage has type EventCallable<string>

const warningMessage = message.prepend<string>((warnMessage) => warnMessage);
// warningMessage has type EventCallable<string>

Typing Stores

Stores can also be typed by passing a type to the generic function, or by specifying a default value during initialization, then TypeScript will infer the type from this value:

import { createStore } from "effector";

// Basic store with primitive value
// StoreWritable<number>
const $counter = createStore(0);

// Store with complex object type
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}`);

Store Types

In Effector, there are two types of stores, where T is the stored value type:

  • Store<T> - derived store type that cannot have new data written to it.
  • StoreWritable<T> - store type that can have new data written using on or sample.

Typing Effects

In normal usage, TypeScript will infer types based on the function’s return result and its arguments. However, createEffect supports typing of input parameters, return result, and errors through generics:

import { createEffect } from "effector";

// Base effect
// 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;
});

Typing Handler Function Outside Effect

If the handler function is defined outside the effect, you’ll need to pass that function’s type:

const sendMessage = async (params: { text: string }) => {
  // ...
  return "ok";
};

const sendMessageFx = createEffect<typeof sendMessage, AxiosError>(sendMessage);
// => Effect<{text: string}, string, AxiosError>

Custom Effect Errors

Some code may only throw certain types of exceptions. In effects, the third generic Fail is used to describe error types:

// Define API error types
interface ApiError {
  code: number;
  message: string;
}

// Create typed effect
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();
});

Typing Methods

sample

Typing filter

If you need to get a specific type, you’ll need to manually specify the expected type, which can be done using type predicates:

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,
});

If you need to check for data existence in filter, you can simply pass Boolean:

import { createEvent, createStore, sample } from "effector";

interface User {
  id: string;
  name: string;
  email: string;
}

// Events
const formSubmitted = createEvent();
const userDataSaved = createEvent<User>();

// States
const $currentUser = createStore<User | null>(null);

// On form submit, send data only if user exists
sample({
  clock: formSubmitted,
  source: $currentUser,
  filter: Boolean, // filter out null
  target: userDataSaved,
});

// Now userDataSaved will only receive existing user data

Typing filter and fn

As mentioned above, using type predicates in filter will work correctly and the correct type will reach the target. However, this mechanism won’t work as needed when using filter and fn together. In this case, you’ll need to manually specify the data type of filter parameters and add type predicates. This happens because TypeScript cannot correctly infer the type in fn after filter if the type isn’t explicitly specified. This is a limitation of TypeScript’s type system.

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>
It got smarter!

Starting from TypeScript version >= 5.5, you don’t need to write type predicates, just specify the argument type and TypeScript will understand what needs to be inferred: filter: (msg: Message) => msg.kind === "user"

attach

To allow TypeScript to infer the types of the created effect, you can add a type to the first argument of mapParams, which will become the Params generic of the result:

const sendTextFx = createEffect<{ message: string }, "ok">(() => {
  // ...

  return "ok";
});

const sendWarningFx = attach({
  effect: sendTextFx,
  mapParams: (warningMessage: string) => ({ message: warningMessage }),
});
// sendWarningFx has type 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>

createApi

To allow TypeScript to infer types of created events, adding a type to second argument of given reducers

const $count = createStore(0);

const { add, sub } = createApi($count, {
  add: (x, add: number) => x + add,
  sub: (x, sub: number) => x - sub,
});

// add has type Event<number>
// sub has type Event<number>

is

is methods can help to infer a unit type (thereby is methods acts as TypeScript type guards) which can help to write strongly-typed helper functions

export function getUnitType(unit: unknown) {
  if (is.event(unit)) {
    // here unit has Event<any> type
    return "event";
  }
  if (is.effect(unit)) {
    // here unit has Effect<any, any> type
    return "effect";
  }
  if (is.store(unit)) {
    // here unit has Store<any> type
    return "store";
  }
}

merge

When we wanna merge events we can get their union types:

import { createEvent, merge } from "effector";

const firstEvent = createEvent<string>();
const secondEvent = createEvent<number>();

const merged = merge([firstEvent, secondEvent]);
// Event<string | number>

// You can also combine events with the same types
const buttonClicked = createEvent<MouseEvent>();
const linkClicked = createEvent<MouseEvent>();

const anyClick = merge([buttonClicked, linkClicked]);
// Event<MouseEvent>

merge accepts generic, where you can use what type do you expect from events:

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>'.

Type Utilities

Effector provides a set of utility types for working with unit types:

UnitValue

The UnitValue type is used to extract the data type from units:

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 is essentially similar to UnitValue, but works only with stores:

import { createStore, StoreValue } from "effector";

const $store = createStore(true);

type StoreValueType = StoreValue<typeof $store>;
// boolean

EventPayload

Extracts the data type from events. Similar to UnitValue, but only for events:

import { createEvent, EventPayload } from "effector";

const event = createEvent<{ id: string }>();

type EventPayloadType = EventPayload<typeof event>;
// {id: string}

EffectParams

Takes an effect type as a generic parameter, allows getting the parameter type of an effect.

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

Takes an effect type as a generic parameter, allows getting the return value type of an effect.

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

Takes an effect type as a generic parameter, allows getting the error type of an effect.

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}
Tarjima jamiyat tomonidan qollanilyapti

Ingliz tilidagi hujjatlar eng dolzarb hisoblanadi, chunki u effector guruhi tomonidan yozilgan va yangilanadi. Hujjatlarni boshqa tillarga tarjima qilish jamiyat tomonidan kuch va istaklar mavjud bo'lganda amalga oshiriladi.

Esda tutingki, tarjima qilingan maqolalar yangilanmasligi mumkin, shuning uchun eng aniq va dolzarb ma'lumot uchun hujjatlarning asl inglizcha versiyasidan foydalanishni tavsiya etamiz.

Hammualliflar