Альфа-Банк corporate blog
JavaScript
Programming
ReactJS
TypeScript
Comments 37
+2
Советую посмотреть на @npm:typesafe-actions Использую полгода на проде, все хорошо.
+2

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

+2
А это хорошая практика? Когда такое стоит применять? Действительно интересно.
+2
Если в вашем проекте используется typescript — то однозначно стоит. Если нет — то можно задуматься об этом) Строгая типизация поможет избежать большого количества ошибок, а внедрять ее в целом несложно. Ну а конкретно про «as const» подробно можно почитать здесь.
+2

Да, используется(не жалеем совсем). Все же дело вкуса(или ситуации) что использовать. Оба подхода имеют свои достоинства и недостатки. Спасибо, есть над чем подумать.

0
Если в экшене кроме типа есть еще какие-то поля с данными, то это становится уже трудно читать.
Потому что поле с данными должно быть только одно — «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;
0

Да вот даже эти примеры из ридми на гитхабе:


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

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

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

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

+3
Запись «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, а не наоборот.

0

Да, тут могут быть разные точки зрения. Но я не хочу увлекаться софистикой) Typescript твердо убежден что "Type '{ foo: string, baz: string }' is not assignable to type 'Foo'."

0

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

0

Проблему #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,
});
0

Нет, не соглашусь. Как раз там возникает описанная проблема в редьюсере: в случае опечатки, без тайпскрипта мы не увидим ошибку, а с тайпскриптом — увидим что-то невнятное.
Есть еще такой подход:


const ACTION_WITH_FOO = 'ACTION_WITH_FOO' as 'ACTION_WITH_FOO';

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

0

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

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

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

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

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

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

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

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

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


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


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

0
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 применится к каждому члену объединения, поэтому из объединения функций получаем объединение возвращаемых ими типов.

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

Only those users with full accounts are able to leave comments. , please.