Home > Software design >  Reducing object of functions to new typed object with varying function signatures
Reducing object of functions to new typed object with varying function signatures

Time:12-03

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!
  • Related