I'm following the patterns shown in Improved Redux type safety with TypeScript 2.8, but I'd like to add a twist. Some of my actions are reusable in different contexts but need a bit of extra identifying information to sort them out in the reducers.
I thought I could solve this and keep my code short by adding some higher-order action creator modifiers. These would take in an existing action creator and return a new action creator that would append some information to the meta key of my Flux Standard Actions.
// 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
}
}
This produces the error:
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'?
While I'll appreciate understanding this error message better, my question is really about what techniques are appropriate for building higher-order action creators.
Is the meta key an appropriate way to implement higher-order action creators? If so, how could I implement addMetaAlpha in a fashion the compiler is happy with? What would a typesafe reducer that handles these augmented actions look like?
The error is a bit misleading, but the cause is that you are trying to assign an object literal where a generic type is expected, A extends FluxStandardAction, but that could mean in addition to type and meta, A could have other members, so the compiler can't really check that the object literal conforms to the shape of A. The following assignment would be valid in the function you wrote because the properties are known and thus it can be checked:
let result : FluxStandardAction<T, M> = { type: type, meta };
If you want to return an object with all the original properties, and the new meta property you could use Object.assign as that will return an intersection type of the type parameters.
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
Also I would not make meta optional, since if the action is augmented the property will exist, I would change the constraint of the function (although you should see how this interacts with the rest of your code base):
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;
As to whether this is a standard way of doing HOC actions, I can't speak to that, but it does seem live a valid way of doing it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With