Comments 39
Да, мы попробовали typesafe-actions перед тем как искать собственное решение, но он не понравился нагромождением своих оберток. Если в экшене кроме типа есть еще какие-то поля с данными, то это становится уже трудно читать. А свежий TS позволяет просто написать as const и всё.
Если в экшене кроме типа есть еще какие-то поля с данными, то это становится уже трудно читать.Потому что поле с данными должно быть только одно — «payload». + вспомогательные «meta» и «error». Это делает код более структурированным и единообразным.
А о каком нагромождении оберток речь? Единственная обертка, которую я смог найти — это ActionCreator, который позволяет делать подобное:
const add = createStandardAction('ADD')<number>();
// In switch reducer
switch (action.type) {
case getType(add):
// action type is { type: "ADD"; payload: number; }
return state + action.payload;
Да вот даже эти примеры из ридми на гитхабе:
export const add = createStandardAction('todos/ADD').map(
(title: string) => ({
payload: { id: cuid(), title, completed: false },
})
);
const add = createCustomAction('todos/ADD', type => {
return (title: string) => ({ type, id: cuid(), title, completed: false });
});
Понятно, что ко всему можно привыкнуть, но зачем?
const createUser = (id: number, name: string) => action('CREATE_USER', { id, name });
import { ActionCreatorsMapObject } from "redux";
interface Action<T extends string> {
type: T
}
interface ActionWithPayload<T extends string, P> extends Action<T> {
payload: P
}
function createAction<T extends string>(type: T): Action<T>
function createAction<T extends string, P>(type: T, payload: P): ActionWithPayload<T, P>
function createAction<T extends string, P>(type: T, payload?:P) {
return payload === undefined ? { type } : { type, payload };
}
export type ActionUnion<T extends ActionCreatorsMapObject> = ReturnType<T[keyof T]>;
И далее используем
const ADD_SOMETHING = '[test] add something';
const CLEAR_ALL = '[test] clear all';
const add = (payload: number) => createAction(ADD_SOMETHING, payload);
const clear = () => createAction(CLEAR_ALL);
const Test = {
add,
clear
};
type Test = ActionUnion<typeof Test>; // ActionWithPayload<"[test] add something", number> | Action<"[test] clear all">
В последующем знатно ругается если ошибся с type либо же с payload для конкретного типа. Выглядят довольно приятно, нет?
У вас в статье так много красного, что хочется поставить двойку.
Запись «T extends string» означает что Т — это некий тип, являющийся подмножеством типа string. Стоит заметить, что это работает так только с примитивными типами — если бы мы использовали вместо string тип объекта с определенным набором свойств, то это бы наоборот означало, что Т является НАДмножеством этого типа.
Нет, это не так. Для объектных типов точно так же получается подмножество.
Возможно, причина непонимания — в следующем. Рассмотрим два типа:
type Foo = { foo: string };
type Bar = { foo: string, bar: number };
Для них выполняется Bar extends Foo
. Может показаться, что тип Foo
— это множество из элемента foo
, а Bar
— из элементов foo
и bar
— но это не так.
На самом деле, Foo
— это множество любых объектов, у которых есть строковое свойство foo
!
К примеру, объект { foo: "Hello, world!", baz: 42 }
всё ещё относится к типу Foo
, несмотря на "лишнее" свойство. А вот к типу Bar
он уже не относится. Поэтому Bar
— подмножество Foo
, а не наоборот.
Да, тут могут быть разные точки зрения. Но я не хочу увлекаться софистикой) Typescript твердо убежден что "Type '{ foo: string, baz: string }' is not assignable to type 'Foo'."
Проблему #1 ещё можно решить, используя строковый enum для action types.
enum ActionType {
ACTION_WITH_FOO = 'ACTION_WITH_FOO',
ACTION_WITH_BAR = 'ACTION_WITH_BAR',
}
const actionCreator1 = () => ({
foo: 'some_value',
type: ActionType.ACTION_WITH_FOO,
});
const actionCreator2 = () => ({
bar: 100500,
type: ActionType.ACTION_WITH_BAR,
});
Нет, не соглашусь. Как раз там возникает описанная проблема в редьюсере: в случае опечатки, без тайпскрипта мы не увидим ошибку, а с тайпскриптом — увидим что-то невнятное.
Есть еще такой подход:
const ACTION_WITH_FOO = 'ACTION_WITH_FOO' as 'ACTION_WITH_FOO';
Тут напрямую, жестко задается литеральный тип. Но это как-то совсем некрасиво, на мой взгляд.
У меня вопрос к сторонникам flow.js: кто-нибудь пробовал реализовать подобное, все работает?
Все давно описано в документации
https://flow.org/en/docs/react/redux/#toc-typing-redux-actions
То что описано в документации не работало для меня т.к. для action типов я использую константы (duck) с шаблонными строками, пример из документации отказывался адекватно работать с константами по какой-то непонятной мне причине.
Но даже если бы он работал, все равно пример из документации недостаточно удобен.
Я решил эту проблему очень просто: взял стейт менеджер, который разрабатывал я с учётом типизации.
Критикую — предлагаю:
// actions.ts
interface IAction<T, R> {
type: T;
payload: R;
}
enum ActionTypes {
CREATE_ITEM = 'CREATE_ITEM'
}
type ItemCreateAction = IAction<ActionTypes.CREATE_ITEM, IItem>;
function itemCreate(item: IItem): ItemCreateAction {
return {
type: ActionTypes.CREATE_ITEM,
payload: item
};
}
const myAction = itemCreate({some: 'item'});
// reducer.ts
function itemsReducer(state = defaultState, action: IAction<ActionTypes>): State {
switch (action.type) {
case ActionTypes.CREATE_ITEM: {
return [...state, action.payload];
}
default:
return state;
}
}
action: IAction<ActionTypes>
как это должно работать, если выше вы объявляете такой интерфейс interface IAction<T, R>?interface IAction<T, R = any> {
type: T;
payload: R;
}
Подробнее можно посмотреть тут:
github.com/shoom3301/react-testing-pyramid/blob/master/src/store/actions/quotes.ts
github.com/shoom3301/angular-testing-pyramid/blob/master/src/store/actions/quotes.action.ts
А у вас саги создают какие-либо экшены минуя action creators? Если нет, то в чем проблема? Если да, то какого фига?
yield all([
takeLatest('SAGA.СLIENT.GET_ID', getClientId),
]);
Потом вызывается генератор getClientId. У тут начинаются непонятки
export function* getClientId(action: any) {
try {
yield console.log(action);
} catch (e) {
console.error(e);
}
};
Какого типа будет action? Не хочется чтобы там был any. И не хочется try catch оборачивать в какие-либо условия в генераторе по типу if (action.payload === 'SAGA.СLIENT.GET_ID').
По факту в action падает ActionTypes. Это и хочется как то использовать. Как вообще поступают?))
import * as action from 'action-creators; type ActionTypes = ReturnType<InferValueTypes<typeof actions>>;
Импортируем action creators как actions, берем их ReturnType (тип возвращаемого значения — экшены), и собираем при помощи нашего специального типа.
Только слегка в другом порядке:
- импортируем action creators как
actions
, получаем хэшмэп функций - с помощью InferValueTypes собираем
actions
в тип-объединение этих функций - Применяем
ReturnType
к этому объединению. Вследствие дистрибутивности этотReturnType
применится к каждому члену объединения, поэтому из объединения функций получаем объединение возвращаемых ими типов.
За статью спасибо, интересная техника.
После советов из комментариев к этой статье, не стали ли вы считать ещё более удачным какой-либо иной способ? Или этот по прежнему самый лучший?
Выведение Action type с помощью Typescript