如何创建类型安全的高阶 Redux 操作创建者?



我遵循了使用TypeScript 2.8改进的Redux类型安全性中显示的模式,但我想添加一个转折点。我的一些操作可以在不同的上下文中重用,但需要一些额外的识别信息才能在化简器中对其进行分类。

我想我可以通过添加一些高阶动作创建器修饰符来解决这个问题并保持我的代码简短。这些将采用现有的操作创建者并返回一个新的操作创建器,该创建器会将一些信息附加到我的 Flux 标准操作的meta键中。

// 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?处理这些增强操作的类型安全化简器会是什么样子?

该错误有点误导,但原因是您正在尝试分配一个对象文本,其中需要泛型类型,A extends FluxStandardAction,但这可能意味着除了typemeta之外,A可能还有其他成员,因此编译器无法真正检查对象文本是否符合A的形状。以下赋值在您编写的函数中有效,因为属性是已知的,因此可以对其进行检查:

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

如果要返回具有所有原始属性的对象,以及可以使用Object.assign的新meta属性,因为这将返回类型参数的交集类型。

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 操作的标准方式,我不能说,但它似乎确实是一种有效的方法。

在为我的动作创建者使用提供的解决方案后,我对化简器的方向略有不同。

我没有尝试在高阶化简器中完全编码不断变化的动作类型,而是使用类型保护来测试动作是否"秘密"携带其中一条元信息。基于此,我可以调用或不调用底层化简器。

// 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;

最新更新