2

I have a Vue 3 project with typescript ( in the process of converting it from javascript to TS), no fancy plugins for vuex store, just simply typed it.

I have a mutation that is setting different properties of a order object, so I don't have to write a mutation for every property. Which looks like this in JS:

 setMetaProp(state, { propName, value }) {
     state.order[propName] = value;
 }

Now, in my process of learning and converting stuff to TS i have written a function that can do this with type safety:

export interface PropNameKeyValue<KeyType, ValueType> {
    propName: KeyType;
    value: ValueType;
}
export declare function setMeta<Key extends keyof Order>(payload: PropNameKeyValue<Key, Order[Key]>): void;

//type of is_quote is boolean in the type declaration of type Order

setMeta({ propName: 'is_quote', value: '123' }); 

//if I try to set is_quote to something else than boolean here
// tsc catches the problem and complains that this should be a boolean

This is awesome and it works as it should be, but if i try to apply this to the mutation I have in the store, type safety is no more, the only thing it does that if i try to set a property that does not exist on type order, at least it tells me that that property does not exist on type Order.

Here is the code I have so far, I will include screenshots as well as to what it shows in the IDE when I hover over each function as well(not sure if it helps tho :)))):

// type declaration of mutations
export type Mutations < S = State > = {
  [MutationTypes.setMetaProp] < PropName extends keyof Order > (
    state: S,
    payload: PropNameKeyValue < PropName, Order[PropName] >
  ): void;
};
// implementation
export const mutations: MutationTree < State > & Mutations = {
  [MutationTypes.setMetaProp](state, {
    propName,
    value
  }) { //i have tried not destructuring, did not change the outcome
    state.order && (state.order[propName] = value);
  },
};
// call the mutation: 
commit(MutationTypes.setMetaProp, {
  propName: 'is_quote',
  value: '123'
}); // no complaints from tsc

//also type declaration of commit  
commit < Key extends keyof Mutations, Params extends Parameters < Mutations[Key] > [1] > (
  key: Key,
  payload: Params
): ReturnType < Mutations[Key] > ;

hover on simple function

Hover on commit

So my question in the end is how can i solve this problem and have type safety?

(If i am here anyways, is there a way change this so i can give the dynamic Type to match the types on, as a type param(so i don't have to type out Order or any type if i have to use it in different places))

UPDATE: Typescript Playground link showcasing problem:

UPDATE2: A retarded solution...(specialCommit ?????):

Basically I declared another type of mutation into AugmentedActionContext and which works only for type Order (most retarded solution, but the only one I could think of)

Update3: In response to answer Playground So basically I would have different kinds of mutations, not only the ones I wanted to have work in the case of the question. Your answer shed some light on what is going on in the background, which i am grateful for. Maybe you have another idea? Or just should i do something like i had in my retarded solution? Basically my retarded solution is similar to yours, i found the problem with that params part just did not know why it did not work. But thank you for your explanation! Also my other problem with this solution is that it limits my usage to only one type for all of commits... Maybe i would like to set properties of a subObject of Order, this will eliminate that option as well.

My new question right now is, am i asking too much of TypeScript and Vuex combo??

Komi
  • 450
  • 5
  • 14

1 Answers1

2

Some times it is hard to get around with all these generics.

In setMeta function TS is able to infer the type because you calling this function.

The problem is in this line:

Parameters<Mutations[Key]>[1]

You cant infer payload without calling functions, at least in this case. TS just does not know what value he should expect.

You can\t infer it with Parameters even from setMeta:

export declare function setMeta<Key extends keyof Order>(payload: PropNameKeyValue<Key, Order[Key]>): void;

type O = Parameters<typeof setMeta>[0] // PropNameKeyValue<keyof Order, any>

The most simplest solution is to make union type of all allowed values:

type Values<T> = T[keyof T]

type AllowedValues = Values<{
  [Prop in keyof Order]: PropNameKeyValue<Prop, Order[Prop]>
}>

Now it works as expected:

import {
  ActionContext,
  ActionTree,
  MutationTree,
} from 'vuex';

type Order = {
  is_quote: boolean;
  order_switches: any;
  api_version: string;
  order_type: string;
  customer_unique_id: string;
  crm_customer_id: string;
  first_name: string;
  last_name: string;
}

type Values<T> = T[keyof T]

type AllowedValues = Values<{
  [Prop in keyof Order]: PropNameKeyValue<Prop, Order[Prop]>
}>

export interface PropNameKeyValue<KeyType, ValueType> {
  propName: KeyType;
  value: ValueType;
}

enum ActionTypes {
  DoStuff = "DO_STUFF"
}
enum MutationTypes {
  setMetaProp = "SET_META_PROP"
}

export type State = {
  order: Order | null;
};

export const state: State = {
  order: null,
};


export const mutations: MutationTree<State> & Mutations = {
  [MutationTypes.setMetaProp](state, payload) {
    state.order && (state.order[payload.propName] = payload.value);
  },
};

export type Mutations<S = State> = {
  [MutationTypes.setMetaProp]<PropName extends keyof Order>(
    state: S,
    payload: PropNameKeyValue<PropName, Order[PropName]>
  ): void;
};


export const actions: ActionTree<State, State> & Actions = {
  [ActionTypes.DoStuff]({ commit }) {
    commit(MutationTypes.setMetaProp, { propName: 'is_quote', value: true });
    commit(MutationTypes.setMetaProp, { propName: 'is_quote', value: 1 }); // expected error

  },
}

interface Actions {
  [ActionTypes.DoStuff](context: AugmentedActionContext): void;
}

type AugmentedActionContext = {
  commit<Key extends keyof Mutations>(
    key: Key,
    payload: AllowedValues // change is here
  ): ReturnType<Mutations[Key]>;
} & Omit<ActionContext<State, State>, 'commit' | 'getters' | 'dispatch'>;

Playground

UPDATE

I overloaded commit function.

Regarding state mutations: TS does not play well with mutations in general. Because objects are contravariant to their keys, string|boolean is evaluated to never, because string & boolean = never.

Please see my article about mutations in typescript.

import {
  ActionContext,
  ActionTree,
  MutationTree,
} from 'vuex';

type Order = {
  is_quote: boolean;
  //  order_switches: any;
  api_version: string;
  order_type: string;
  customer_unique_id: string;
  crm_customer_id: string;
  first_name: string;
  last_name: string;
}
type Artwork = {
  artwork_id: number;
  artwork_size: string;
}

type Values<T> = T[keyof T]

type AllowedValues<Type> = Values<{
  [Prop in keyof Type]: {
    propName: Prop;
    value: Type[Prop];
  }
}>

export interface PropNameKeyValue<KeyType, ValueType> {
  propName: KeyType;
  value: ValueType;
}

enum ActionTypes {
  DoStuff = "DO_STUFF"
}
enum MutationTypes {
  setMetaProp = "SET_META_PROP",
  setArtworkProp = "SET_ARTWORK_PROP",
  setRandomVar = "SET_RANDOM_VAR",
}

export type State = {
  order: Order | null;
  artwork: Artwork | null;
  randomVar: string;
};



export const state: State = {
  order: null,
  artwork: null,
  randomVar: '',
};

function setMeta<S extends Values<State>, Key extends keyof S>(state: S, payload: AllowedValues<S>) { }


export const mutations: MutationTree<State> & Mutations = {
  [MutationTypes.setMetaProp]: (state, payload) => {
    const x = state.order as Order

    if (state.order) {
      // TS is unsure about safety
      const q = state.order[payload.propName] // string|boolean
      const w = payload.value // string | boolean"
      /**
       * Because both
       * state.order[payload.propName] and payload.value
       * evaluated to stirng | boolean
       * TS thinks it is not type safe operation
       * 
       * Pls, keep in mind, objects are contravariant in their key types
       */
      // workaround
      Object.assign(state.order, { [payload.propName]: payload.value })
    }

    if (payload.propName === 'api_version') {
      state.order && (state.order[payload.propName] = payload.value);
    }

    state.order && (state.order[payload.propName] = payload.value);
  },
  [MutationTypes.setArtworkProp](state, payload) {
    state.artwork && (state.artwork[payload.propName] = payload.value);
  },
  [MutationTypes.setRandomVar](state, payload) {
    state.randomVar = payload;
  },
};



export type Mutations<S = State> = {
  [MutationTypes.setMetaProp]: (
    state: S,
    payload: AllowedValues<Order>
  ) => void;
  [MutationTypes.setArtworkProp]: (
    state: S,
    payload: AllowedValues<Artwork>
  ) => void;
  [MutationTypes.setRandomVar]: (
    state: S,
    payload: string
  ) => void; //added these mutations: setArtworkProp and setRandomVar, they will be unusable from now on...
};


export const actions: ActionTree<State, State> & Actions = {
  [ActionTypes.DoStuff]({ commit }) {
    commit(MutationTypes.setMetaProp, { propName: 'is_quote', value: true });
    commit(MutationTypes.setArtworkProp, { propName: 'artwork_id', value: 2 });
    commit(MutationTypes.setArtworkProp, { propName: 'artwork_id', value: '2' });// expected error

    commit(MutationTypes.setRandomVar, '2');
    commit(MutationTypes.setRandomVar, 2); // expected error
  },
}

interface Actions {
  [ActionTypes.DoStuff](context: AugmentedActionContext): void;
}

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

type Overloading = UnionToIntersection<Values<{
  [Prop in keyof Mutations]: {
    commit(
      key: Prop,
      payload: Parameters<Mutations[Prop]>[1]
    ): ReturnType<Mutations[Prop]>
  }
}>>

type AugmentedActionContext = Overloading & Omit<ActionContext<State, State>, 'commit' | 'getters' | 'dispatch'>;
Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
  • You answer helped me shed some light on what is going on in the background, thank you for that, but it removes the possibility for me to have different types of mutations (Added new playground link with examples of how i use this, now dont get me wrong, i dont intend to have these really different types of entities in one store, violating principles and stuff, just as an example) I will have different mutations like setting a single variable and the likes, this totally impedes this kind of usage. – Komi Jun 29 '21 at 13:35
  • Amazing!!!! This is exactly what i wanted. Thank you so much! How and where can i learn these powers of typescript you have? – Komi Jun 29 '21 at 16:43
  • 1
    1) start with https://www.typescriptlang.org/docs/handbook/intro.html 2) follow `typescript` tag on StackOverflow - I learn a lot from SO 3) check my blog https://catchts.com/ , I'm triyng to write about every interesting question/answer regarding TS types – captain-yossarian from Ukraine Jun 29 '21 at 16:52