Best practices for writing well-typed code
createEvent
By default, this method returns Event<void>
.
const event = createEvent();
// event has type Event<void>
event();
Event type can be defined as generic
const event = createEvent<number>();
// event has type Event<number>
event(0);
createEffect
TypeScript can infer an effect result type from a given handler, but the argument type should be defined either in handler argument or as generic type
const sendMessageFx = createEffect(async (params: { text: string }) => {
// ...
return "ok";
});
// sendMessageFx has type Effect<{text: string}, string>
const sendWarningFx = createEffect<{ warn: string }, string>(async ({ warn }) => {
// ...
return "ok";
});
// sendWarningFx has type Effect<{warn: string}, string>
createEffect
and custom errors
When you need custom error types (Fail
type in Effect
) you can define all generics explicitly:
const sendWarningFx = createEffect<{ warn: string }, string, AxiosError>(async ({ warn }) => {
// ...
return "ok";
});
// sendWarningFx has type Effect<{warn: string}, string, AxiosError>
In case when effect’s handler is defined before effect itself you can allow typescript to infer the type of Params
and Done
by using typeof handler
in first generic and optionally provide Fail
type as second one
const sendMessage = async (params: { text: string }) => {
// ...
return "ok";
};
const sendMessageFx = createEffect<typeof sendMessage, AxiosError>(sendMessage);
// sendMessageFx has type Effect<{text: string}, string, AxiosError>
event.prepend
To add types to events, created by event.prepend, you need to add a type either by prepending function argument or as generic type
const message = createEvent<string>();
const userMessage = message.prepend(({ text }: { text: string }) => text);
// userMessage has type Event<{text: string}>
const warningMessage = message.prepend<{ warn: string }>(({ warn }) => warn);
// warningMessage has type Event<{warn: string}>
attach
To allow typescript to infer types of created effect, add a type to mapParams
first argument, which will become effect params
type
const sendTextFx = createEffect<{ text: string }, "ok">();
const sendWarningFx = attach({
effect: sendTextFx,
mapParams: ({ warn }: { warn: string }) => ({ text: warn }),
});
// sendWarningFx has type Effect<{warn: string}, 'ok'>
split
TypeScript type predicates can be used to split a common event type to several cases (hence the name)
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 has type Event<UserMessage>
// warnMessage has type Event<WarnMessage>
sample
Since effector@22.2.0
update sample
also supports a filter
field, which can also be a TypeScript type predicate.
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 + fn
However, sample
also has a fn
field to apply custom transformations. There is a caveat with TypeScript type inference mechanic, which requires user to explicitly type filter
arguments for type inference to work
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,
// need to explicitly type `msg` as `Message` there
filter: (msg: Message): msg is UserMessage => msg.kind === "user",
// to get correct type inference here
fn: (msg) => msg.text,
target: userText,
});
// userMessage has type Event<string>
Otherwise, TypeScript will fall back to any
.
However, TypeScript will not allow you to set an incorrect filter
type
const message = createEvent<Message>();
const userMessage = createEvent<UserMessage>();
sample({
clock: message,
// Type 'Message' is not assignable to type '{ kind: "user" | "wrong"; text: number; }'.
filter: (msg: { kind: "user" | "wrong"; text: number }): msg is UserMessage =>
msg.kind === "user",
fn: (msg) => msg.text,
target: userMessage,
});
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";
}
}