0

I'm trying to create a function to generate my redux action creators.
Let's say we have these types for our actions:

export type DeleteAction = {
  type: typeof BLAH_DELETE;
  payload: {
    id: string;
  };
};

export type EditAction = {
  type: typeof BLAH_EDIT;
  payload: {
    id: string;
    name: string;
  };
};

export MyAction = DeleteAction | EditAction

Now in the actions file, I would like to create my actions in this way:

export const deleteBlah = makeActionCreator<MyAction>('BLAH_DELETE');

// Expected Behaviour 
deleteBlah({ id: '' }) // Correct
deleteBlah({ id: '', name: '' }) // Error

export const editBlah = makeActionCreator<MyAction>('BLAH_EDIT');

// Expected Behaviour 
editBlah({ id: '', name: '' }) // Correct
editBlah({ id: '' }) // Error

Here is the makeActionCreator function:

export const makeActionCreator = <A extends { type: string; payload: any }>(type: A['type']) => (
  payload: ActionPayload<ExtractAction<A, A['type']>>,
) => ({
  type,
  payload,
});

type ExtractAction<A, T> = A extends { type: T } ? A : never;

type ActionPayload<T extends { payload: any }> = Pick<T['payload'], keyof T['payload']>;

The problem is I don't know how can I pass the action type which is provided in actions file to the ExtractAction<A, A['type']> so, the payload is always valid for all possible options of A.

11
  • How does the type ActionPayload look like? I guess it would be: type ActionPayload<A> = A extends { payload: any } ? A['payload'] : never; Commented Aug 10, 2020 at 7:26
  • Don't know why you specify correct type corresponding to function like: export const deleteBlah = makeActionCreator<DeleteAction>('BLAH_DELETE'); Commented Aug 10, 2020 at 7:30
  • @tmhao2005 Sorry I just added that one as well. Because of 2 reasons: 1) Reduce the number of imports and simplify the file. 2) Avoid repeating unnecessary data. DeleteAction itself has the action type so it doesn't make sense to send it again as a param, but we need that param as well :) Commented Aug 10, 2020 at 7:33
  • Ah. you meant that you can infer the payload based on the input action key? Commented Aug 10, 2020 at 7:36
  • @tmhao2005 YUP! :) Commented Aug 10, 2020 at 7:37

2 Answers 2

1

Eventually, I have some idea to turn your idea work. Here are the few steps:

  • First of all, we define some utils type which we can detect the type of any prop & extract the type of payload property as well:
type ValueType<T, K> = K extends keyof T ? T[K] : never;

type ExtractPayload<A, T> = A extends { type: T, payload: infer R } ? R : never;

  • Next, we define the return Curry function which receives action as argument:
type Curry<A> = <T extends ValueType<A, 'type'>>(arg: T) => (payload: ExtractPayload<A, T>) => {
  type: T
  payload: ExtractPayload<A, T>
};

  • Finally, we re-write your action creator function a bit since I don't know how to declare the type for a function rather than just define only return type, so you have to create one more level of curry makeActionCreator<MyAction>() which is a bit annoying though:
export const makeActionCreator = <A>(): Curry<A> => action => payload => ({
  type: action,
  payload,
})

// Testing

const deleteBlah = makeActionCreator<MyAction>()('BLAH_DELETE');
deleteBlah({ id: '' }) // Correct
deleteBlah({ id: '', name: '' }) // Error

Another solution

Another options, you would keep your function without creating more curry level but you have to pass one more typing argument as following:

type Curry1<A, T> = (payload: ExtractPayload<A, T>) => {
  type: T
  payload: ExtractPayload<A, T>
};

// The function is the same but have type T as new parameter

export const makeActionCreator = <A, T>(action: T): Curry1<A, T> => payload => ({
  type: action,
  payload,
})

// Testing, it's a bit odd as specify 'BLAH_DELETE' twice

const deleteBlah = makeActionCreator<MyAction, 'BLAH_DELETE'>('BLAH_DELETE');
deleteBlah({ id: '' }) // Correct
deleteBlah({ id: '', name: '' }) // Error

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks! This one is also a good solution...But I can't mark it as an answer because it's not exactly what I want. If I find that it's not possible to apply my idea in TS, I'll mark it as an answer ;)
I dropped another proposal :) but not perfect I guess since having input more argument
0
type Some<U, I> = (U extends I ? true : false) extends false ? false : true;

function makeCreator<T extends MyAction["type"]>(type: T) {
  return function <P>(
    p: { type: T; payload: P } extends MyAction
      ? Some<MyAction, { type: T; payload: P }> extends true
        ? P
        : never
      : never
  ): MyAction {
    return null as any;
  };
}

code

 { type: T; payload: P } extends MyAction
      ? OneOf<MyAction, { type: T; payload: P }> extends true
        ? P
        : never
      : never`

assert the argument you provided is exactly the payload type according type given

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.