Hamid
Hamid

Reputation: 2088

TypeScript use generic function parameter in curry function

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.

Upvotes: 0

Views: 307

Answers (2)

tmhao2005
tmhao2005

Reputation: 17504

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

Upvotes: 1

钵钵鸡实力代购
钵钵鸡实力代购

Reputation: 1042

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

Upvotes: 0

Related Questions