Как стать автором
Обновить

Комментарии 39

Советую посмотреть на @npm:typesafe-actions Использую полгода на проде, все хорошо.

Да, мы попробовали typesafe-actions перед тем как искать собственное решение, но он не понравился нагромождением своих оберток. Если в экшене кроме типа есть еще какие-то поля с данными, то это становится уже трудно читать. А свежий TS позволяет просто написать as const и всё.

А это хорошая практика? Когда такое стоит применять? Действительно интересно.
Если в вашем проекте используется typescript — то однозначно стоит. Если нет — то можно задуматься об этом) Строгая типизация поможет избежать большого количества ошибок, а внедрять ее в целом несложно. Ну а конкретно про «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 });
});

Понятно, что ко всему можно привыкнуть, но зачем?

Никто не заставляет использовать их action creator'ы, можно писать самому:
const createUser = (id: number, name: string) => action('CREATE_USER', { id, name });
Вставлю уж и свои 5 серебряников нарытых на бескрайних просторах:

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 для конкретного типа. Выглядят довольно приятно, нет?
Или github.com/pelotom/unionize где еще матчинг сразу из коробки.

Посмотрю, что это такое, спасибо)

У вас в статье так много красного, что хочется поставить двойку.

Спасибо за замечание, учтем)
Это корпоративный цвет )
Запись «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'."

Typescript 3.4.2
image

А, это просто известный костыль для литералов объектов (во второй строчке об этом и пишут). Попробуйте через дополнительную переменную.

Проблему #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';

Тут напрямую, жестко задается литеральный тип. Но это как-то совсем некрасиво, на мой взгляд.

Зачем тут as? Простой const ACTION_WITH_FOO = 'ACTION_WITH_FOO' точно так же задаёт литеральный тип, потому что для const нету type widening в отличие от let/var.

Спасибо большое за статью. Описанные проблемы действительно существуют, куча статей написана, люди продолжают искать наиболее оптимальный способ типизации в Redux. Не могу сказать, что решение идеально, но в сравнении с другими выглядит довольно неплохо.
У меня вопрос к сторонникам flow.js: кто-нибудь пробовал реализовать подобное, все работает?
Вариант описанный в статье выглядит более удобным.
То что описано в документации не работало для меня т.к. для action типов я использую константы (duck) с шаблонными строками, пример из документации отказывался адекватно работать с константами по какой-то непонятной мне причине.
Но даже если бы он работал, все равно пример из документации недостаточно удобен.

Я решил эту проблему очень просто: взял стейт менеджер, который разрабатывал я с учётом типизации.

Писать велосипеды хорошо для самообразования и допустимо только в личных проектах. Когда появляется желание написать свой Redux, подумайте о своей команде, которой придется все это изучать. Вы искусственно завышаете порог вхождения в проект.

Прошу прощения. Автокоррекция сработала, а я не заметил ошибки. Стейт менеджер я самостоятельно для проекта не писал. Мы используем эффектор, который прекрасно типизируется.
У него хоть и есть проблемы, но это то решение, которое на голову выше любого из текущих стейт менеджеров.

Возможно я не понял, но зачем так усложнять?
Критикую — предлагаю:
// 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
Таким образом у Вас теряется тип payload'а в редьюсере, он будет «any».
Вроде как, ваш вариант сложнее в использовании, ибо здесь нужно на каждый action определить тип и написать фабричную функцию. А у Дмитрия пишется только фабричная функция.
Подскажите, пожалуйста, а как быть, если я использую саги?

А у вас саги создают какие-либо экшены минуя action creators? Если нет, то в чем проблема? Если да, то какого фига?

Action creator запускает сагу, условно такую
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. Это и хочется как то использовать. Как вообще поступают?))

Не вижу в вашем коде создания экшена, только его обработку.


Если вам не хочется чтобы параметром getClientId был any — не пишите any. Пишите правильный тип.


Только надо будет написать правильную версию takeLatest, чтобы она этот тип проверяла.

import * as action from 'action-creators;
type ActionTypes = ReturnType<InferValueTypes<typeof actions>>;


Импортируем action creators как actions, берем их ReturnType (тип возвращаемого значения — экшены), и собираем при помощи нашего специального типа.

Только слегка в другом порядке:


  1. импортируем action creators как actions, получаем хэшмэп функций
  2. с помощью InferValueTypes собираем actions в тип-объединение этих функций
  3. Применяем ReturnType к этому объединению. Вследствие дистрибутивности этот ReturnType применится к каждому члену объединения, поэтому из объединения функций получаем объединение возвращаемых ими типов.

За статью спасибо, интересная техника.

После советов из комментариев к этой статье, не стали ли вы считать ещё более удачным какой-либо иной способ? Или этот по прежнему самый лучший?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий