Search code examples
typescriptvue.jstypesvuex

Generic type for typing vuex store


I am trying to create a generic type which will prove typing hints for Vuex mutation. I read this article Vuex + TypeScript and I get inspired to create something more generic. I came up with something like this:

export type MutationType<S, P, K extends keyof P> = Record<K, (state: S, payload: P[K]) => void>;

// S
export type DiceGameState = {
  round: number;
  score: number;
  diceItems: DiceHistoryItem[];
};

// P
export interface DiceGameMutationPayloadMap {
  INCREMENT_SCORE: number;
  DECREMENT_ROUND: number;
  ADD_DICE_HISTORY_ITEM: DiceHistoryItem;
}

// K
export enum DiceGameMutationsKeys {
  INCREMENT_SCORE = 'INCREMENT_SCORE',
  DECREMENT_ROUND = 'DECREMENT_ROUND',
  ADD_DICE_HISTORY_ITEM = 'ADD_DICE_HISTORY_ITEM'
}

export type DiceStoreMutation = MutationType<
  DiceGameState,
  DiceGameMutationPayloadMap,
  keyof typeof DiceGameMutationsKeys
>;

export const mutations: MutationTree<DiceGameState> & DiceStoreMutation = {
  DECREMENT_ROUND: (state: DiceGameState, payload: number) => {},
  INCREMENT_SCORE: (state: DiceGameState, payload: number) => {},
  ADD_DICE_HISTORY_ITEM: (state: DiceGameState, payload: DiceHistoryItem) => {}
};

Where:

  • S → type of mutation state
  • P → map of mutation name and type of payload assigned to this mutation
  • K → mutation name

If I check details of this MutationType in my IDE I am getting this hint:

enter image description here

So the key in this object is one of K keys and the value is a method which takes state which is type of S and payload which is type of assigned value to P.

If I try to compile this code I am getting this error:

Error:(7, 3) TS2322: Type '(state: DiceGameState, payload: number) => void' is not assignable to type '(state: DiceGameState, payload: number | DiceHistoryItem) => void'. Types of parameters 'payload' and 'payload' are incompatible. Type 'number | DiceHistoryItem' is not assignable to type 'number'. Type 'DiceHistoryItem' is not assignable to type 'number'.

Do you know how can I make my generic to work ?

Example:

I want to let TS infer the type of payload for a given mutation name. For example if you have a mutation named INCREMENT_SCORE then the type of payload hinted by TS should be a number. This is the reason why I created this: DiceGameMutationPayloadMap, then after creating this const mutations. Typescript should tell me what type payload should have (basing on name of mutation).

A good example of this can be also typing for addEventListenr method provided by TypeScript. This type infers the type of event (ev) basing on a given event name(type):

addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLButtonElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;

For more details about this type you should check lib.dom.ts → line 6385


Solution

  • Actually, it is possible. It takes me some time to figure out what I am doing wrong but in the end I understood my mistake.

    My first mistake was inside DiceStoreMutation because my 3rd argument of this generic referred to type of the key no key name. The proof of this we can see on attached screenshot:

    enter image description here

    We can see that Expanded shows that p is actually a string, but it should be a union of string Literals 'ADD_DICE_HISTORY_ITEM' | 'DECREMENT_ROUND' | 'INCREMENT_SCORE'. You could think that maybe I just should use keyof DiceGameMutationsKeys instead of keyof typeof DiceGameMutationsKeys which will refer to keys names of this interface instead of types of keys, but this of course occurred an error as expected. It made me think that I should change my generic MutationType somehow. After I read @Joel Bourbonnais answers and check this very good example about the P[K] part being a Index types. I came up with a new idea for my MutationType: Mapped generics + Index types. This new type came up with this shape:

    export type MutationType<T, U> = {
      [K in keyof U]: (state: T, payload: U[K]) => void;
    };
    

    And I can say only one thing It works a treat. Now I do not even need DiceGameMutationsKeys because DiceGameMutationPayloadMap provides all the information. You may ask why? I'm already rushing with an explanation:

    • Firstly, K in keyof U so K can be only a key of U so it will be a union of sting literals of U keys interface.
    • Secondly, U[K] so it will be a lookup for a type of U under the K key(one of this union of sting literals of U keys interface)

    Below you can find a screenshot with proof:

    enter image description here