I'm refactoring some React/Redux code that used to use an old pattern into a more "hooks" based pattern, using TypeScript.
In the old pattern, we'd use "class-based" components, and pass in their "dispatch" functions using mapDispatchToProps
:
const mapDispatchToProps = {
doSomething: someSomethingAction,
doSomething: doSomethingElseAction
};
const mapStateToProps = state => {
return {
...
};
};
export const MyComponent = connect(
mapStateToProps,
mapDispatchToProps
)(UnconnectedComponent);
Then in the component, you'd access this.props.doSomething()
, etc.
In the new pattern, I'd do this:
const dispatch = useDispatch();
const doSomething = (...args: any[]) => dispatch(doSomethingAction(...args));
const doSomethingElse = (...args: any[]) => dispatch(doSomethingElseAction(...args));
I'd love to refactor this, so that it doesn't have to be so damn verbose (some component have like 20 of these functions), and also so that I can automatically get the types of the action
function (eg doSomethingAction
) to be the types of the "dispatched" function (eg doSomething
).
The former is easy enough. I'd just do something like this:
const useActions = (actionObj) => {
const dispatch = useDispatch();
return Object.keys(actionObj).reduce((acc, key) => ({
...acc,
[key.replace(/Action$/g, '')]: (...args) => dispatch(actionObj[key](...args))
}, {});
}
The trouble is, the above is JavaScript, and doesn't handle any of the typing. I'd love to have a function that could "extract" these types automatically, so that the extracted "dispatch" functions pulled from the resulting object know how many args, and of what types, to take.
I was thinking maybe I could do something like the below, but I'm running into issues and don't really know if this is possible, so I'd appreciate any feedback:
const useActions = (actionObj: Record<string, (...args: unknown[]) => unknown>) => {
const dispatch = useDispatch();
return Object.keys(actionObj).reduce((acc, key) => ({
...acc,
[key.replace(/Action$/g, '']: (...args) => (dispatch(actionObj[key](...args)) as Parameters<typeof actionObj[key]>)
}, {});
}
Thoughts? Is this even possible with individual actions? Eg a const doSomething = useDispatchAction(doSomethingAction);
that extracts the right parameter types?
EDIT
I suppose this question could abstract away more of the React context.
Basically, the question is, with a function like the below, can I create A) a "wrapping" function which returns a new function which takes the same args as the original, automatically, B) even better, a "wrapping" function which takes multiple functions (as an array or object), "wraps" them as in the above, and returns a set of functions which have typed arguments:
const exampleFunc = (first: string, second: number) => `${first} #${second}`;
// how to define?
const wrapperTypeOne = '???';
const wrapped = wrapperTypeOne(exampleFunc);
wrapped('hello', 2); // works
wrapped('hello'); // fails type check
wrapped(2, 2); // fails type check
// Version 2 - how to define
const wrapperTypeTwo = '???';
const wrapped2 = wrapperTypeTwo({ exampleFunc });
// Or, allow for name modification, but that's the easy part
wrapped2.exampleFunc('hello', 2); // works
wrapped2.exampleFunc('hello'); // fails type check
wrapped2.exampleFunc(2, 2); // fails type check
CodePudding user response:
As long as you're willing to put up with a little bit of any
trust-me-I-know-what-I'm-doing in the construction of this type, both sides of the construction can be well-typed:
type ActionsProps<T> = {
[K in keyof T as ActionlessKey<K & string>]: T[K]
}
type ActionlessKey<K extends string> = K extends `${infer V}Action` ? V : never;
function useActions<T extends object>(actions: T): ActionsProps<T> {
const dispatch = useDispatch();
const keys = Object.keys(actions);
return keys.reduce((acc, key) => ({
...acc,
[key.replace(/Action$/g, '')]: (...args: any[]) => (dispatch((actions as any)[key](...args)))
}), {}) as any;
}
Usage is as simple as:
enum Dinosaur {
T_REX,
STEGOSAURUS,
PTERODACTYL,
TRICERATOPS,
// There are obviously _MANY_ more!
}
type UserInfoActionArgs = { name: string, age: number, favoriteDinosaur: Dinosaur };
const userInfoAction = ({name, age, favoriteDinosaur}: UserInfoActionArgs) => {
return { payload: { name, age, favoriteDino: favoriteDinosaur } };
}
const myActions = useActions({
fooAction: (x: number, y: number) => ({payload: x y}),
barAction: userInfoAction
})
myActions.foo(3, 4);
myActions.bar({ name: 'Sam', age: 3 });
// That's an error - must remember the favorite DINOSAUR!