Как я могу создать безопасных высокоуровневых создателей действий Redux? - PullRequest
0 голосов
/ 13 июня 2018

Я следую шаблонам, показанным в Улучшена безопасность типов Redux с TypeScript 2.8 , но я бы хотел добавить поворот.Некоторые из моих действий можно использовать повторно в разных контекстах, но для их сортировки в редукторах требуется немного дополнительной идентифицирующей информации.

Я думал, что смогу решить эту проблему и сократить код, добавив создателя действий более высокого порядка.модификаторы.Они будут принимать существующего создателя действий и возвращать нового создателя действий, который добавит некоторую информацию к ключу meta моих стандартных действий Flux.

// Removed `payload` and `error` for this reproduction
interface FluxStandardAction<T extends string, Meta> {
    type: T;
    meta?: Meta;
}

// Our basic actions

enum ActionType {
    One = "one",
    Two = "two",
}

interface Action1 {
    type: ActionType.One;
}

interface Action2 {
    type: ActionType.Two;
}

type Action = Action1 | Action2;

function action1(): Action1 {
    return { type: ActionType.One }
}

function action2(): Action2 {
    return { type: ActionType.Two }
}

// Higher order action modifiers that augment the meta

interface MetaAlpha {
    meta: {
        alpha: string;
    }
}

function addMetaAlpha<T extends string, M extends {}, A extends FluxStandardAction<T, M>>(action: () => A, alpha: string) {
    return function (): A & MetaAlpha {
        let { type, meta } = action();
        return { type, meta }; // Error here
    }
}

Это приводит к ошибке:

Type '{ type: T; meta: M; }' is not assignable to type 'A & MetaAlpha'.
  Object literal may only specify known properties, but 'type' does not exist in type 'A & MetaAlpha'. Did you mean to write 'type'?

Хотя я буду признателен за лучшее понимание этого сообщения об ошибке, мой вопрос на самом деле о том, какие методы подходят для создания создателей действий более высокого порядка.

Является ли ключ meta подходящим способом реализации более высокого уровня?Порядок действий создателей?Если да, то как я могу реализовать addMetaAlpha так, чтобы компилятор был доволен?Как бы выглядел типобезопасный редуктор, который обрабатывает эти расширенные действия?

Ответы [ 2 ]

0 голосов
/ 14 июня 2018

После использования предоставленного решения для моих создателей действий я пошел в несколько ином направлении для редукторов.

Вместо того, чтобы пытаться полностью кодировать изменяющиеся типы действий вредукторы высшего порядка, я использую охранники типов, чтобы проверить, является ли действие «тайным», несущим одну из частей метаинформации.Исходя из этого, я могу вызвать базовый редуктор или нет.

// Basic actions

// Removed payload and error for this demonstration
interface FluxStandardAction<T extends string, Meta = undefined> {
    type: T;
    meta?: Meta;
}

enum ActionType {
    One = "one",
    Two = "two",
}

const action1 = () => ({ type: ActionType.One });
const action2 = () => ({ type: ActionType.Two });

type Action =
    | ReturnType<typeof action1>
    | ReturnType<typeof action2>
    ;

// Higher order action modifiers that augment the action's meta properties

interface WithGreekLetter {
    meta: {
        greek: string;
    }
}

const withGreekLetter = <T extends string, M extends {}, A extends FluxStandardAction<T, M>>(action: () => A, greek: string) =>
    (): A & WithGreekLetter => {
        let act = action();
        let meta = Object.assign({}, act.meta, { greek });
        return Object.assign(act, { meta });
    }

const isWithGreekLetter = (a: any): a is WithGreekLetter =>
    a['meta'] && a['meta']['greek'];

// A basic reusable reducer

type State = number;
const initialState: State = 0;

function plainReducer(state: State, action: Action): State {
    switch (action.type) {
        case ActionType.One:
            return state + 1;
        case ActionType.Two:
            return state + 2;
        default:
            return state;
    }
}

// The higher-order reducer

const forGreekLetter = <S, A>(reducer: (state: S, action: A) => S, greek: string) =>
    (state: S, action: A) =>
        isWithGreekLetter(action) && action.meta.greek === greek ? reducer(state, action) : state;

// Build the concrete action creator and reducer instances

const ALPHA = 'alpha';
const BETA = 'beta';

let oneA = withGreekLetter(action1, ALPHA);
let oneB = withGreekLetter(action1, BETA);
let twoA = withGreekLetter(action2, ALPHA);
let twoB = withGreekLetter(action2, BETA);

let reducerAlphaNoInitial = forGreekLetter(plainReducer, ALPHA);
let reducerA = (state = initialState, action: Action) => reducerAlphaNoInitial(state, action);

let reducerBetaNoInitial = forGreekLetter(plainReducer, BETA);
let reducerB = (state = initialState, action: Action) => reducerBetaNoInitial(state, action);

// Exercise the action creators and reducers

let actions = [oneB(), oneA(), twoB(), twoA(), twoB()];

let stateA: State | undefined = undefined;
let stateB: State | undefined = undefined;

for (const action of actions) {
    stateA = reducerA(stateA, action);
    stateB = reducerB(stateB, action);
}

console.log({ stateA, stateB });
// {stateA: 3, stateB: 5}

Я также экспериментировал с попыткой использовать систему типов более полно, но я обнаружил, что мне нужно добавить прочее в любом случае наберите охрану, чтобы перейти от общего FluxStandardAction к моему конкретному действию.Поскольку для этого мне нужно было использовать средства защиты типов, я чувствовал, что другой путь был проще.

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

const isAction = (a: any): a is Action =>
    Object.values(ActionType).includes(a['type']);

export const onlySpecificAction = <S, A1, A2>(reducer: (s: S, a: A1) => S, isA: IsA<A1>) =>
    (state: S, action: A2) =>
        isA(action) ? reducer(state, action) : state;
0 голосов
/ 13 июня 2018

Ошибка немного вводит в заблуждение, но причина в том, что вы пытаетесь присвоить литерал объекта там, где ожидается универсальный тип, A extends FluxStandardAction, но это может означать в дополнение к type и meta, A может иметь другие члены, поэтому компилятор не может реально проверить, соответствует ли литерал объекта форме A.Следующее присваивание будет действительным в написанной вами функции, поскольку свойства известны и, следовательно, его можно проверить:

let result : FluxStandardAction<T, M> = { type: type, meta };

Если вы хотите вернуть объект со всеми исходными свойствами и новым meta свойство, которое вы можете использовать Object.assign, поскольку оно будет возвращать тип пересечения параметров типа.

function addMetaAlpha<A extends FluxStandardAction<string, any>>(action: () => A, alpha: string) {
    return function (): A & MetaAlpha {
        let act = action();
        return Object.assign(act, {
            meta: {
                alpha
            }
        })
    }
}

var metaAct1 = addMetaAlpha(action1, "DD"); // () => Action1 & MetaAlpha

Также я бы не стал делать meta необязательным, поскольку, если действие дополнено, свойство будет существовать, я бы изменил ограничение функции (хотя вы должны увидеть, как это взаимодействует с остальнымиваша база кода):

function addMetaAlpha<A extends { type: string }>(action: () => A, alpha: string) {
    return function (): A & MetaAlpha {
        let act = action();
        return Object.assign(act, {
            meta: {
                alpha
            }
        })
    }
}

var metaAct1 = addMetaAlpha(action1, "DD"); // () => Action1 & MetaAlpha
var metaAct2 : () => FluxStandardAction<'one', { alpha: string }> =  metaAct1; // Is is assignable to the interface if we need it to be;

Что касается того, является ли это стандартным способом выполнения действий HOC, я не могу говорить об этом, но, похоже, это действительно правильный способ сделать это.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...