如何在 vuex store typescript 中动态设置对象的属性(同时保持类型安全)

How to set properties of object dinamically (while keeping type safety) in vuex store typescript

我有一个带有打字稿的 Vue 3 项目(正在将它从 javascript 转换为 TS),没有用于 vuex 商店的花哨插件,只是简单地输入它。

我有一个设置订单对象不同属性的变更,所以我不必为每个 属性 编写一个变更。在 JS 中看起来像这样:

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

现在,在我学习并将内容转换为 TS 的过程中,我编写了一个可以在类型安全的情况下执行此操作的函数:

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

这太棒了,它应该能正常工作,但是如果我尝试将它应用于商店中的突变,类型安全就不再存在,如果我尝试设置一个属性 在类型订单上不存在,至少它告诉我 属性 在类型订单上不存在。

这是我目前的代码,当我将鼠标悬停在每个函数上时,我将包括屏幕截图以及它在 IDE 中显示的内容(不确定它是否有帮助:)) )):

// 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] > ;

所以我最后的问题是如何解决这个问题并确保类型安全?

(如果我无论如何都在这里,有没有办法改变这个,所以我可以给动态类型匹配类型,作为类型参数(所以我不必输入 Order 或任何类型,如果我必须在不同的地方使用它))

更新:Typescript Playground link showcasing problem:

更新 2:A retarded solution...(specialCommit ?????):

基本上我在 AugmentedActionContext 中声明了另一种类型的突变,它只适用于 Order 类型(最迟钝的解决方案,但我能想到的唯一一个)

更新 3:回应回答 Playground 所以基本上我会有不同种类的突变,而不仅仅是我想在问题的情况下起作用的突变。您的回答阐明了背景中发生的事情,对此我深表感谢。也许你有另一个想法?或者我应该像我的弱智解决方案中那样做一些事情吗?基本上我的弱智解决方案与你的类似,我发现那个参数部分的问题只是不知道为什么它不起作用。不过谢谢你的解释!这个解决方案的另一个问题是它限制我对所有提交的使用只能使用一种类型...也许我想设置 Order 子对象的属性,这也将消除该选项。

我现在的新问题是,我对 TypeScript 和 Vuex 组合的要求是不是太多了??

有时很难使用所有这些泛型。

setMeta 函数中 TS 能够推断出类型,因为你调用了这个函数。

问题出在这一行:

Parameters<Mutations[Key]>[1]

你不能在不调用函数的情况下推断有效载荷,至少在这种情况下是这样。 TS只是不知道他应该期待什么价值。

你 can\t 用 Parameters 推断它,甚至从 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>

最简单的解决方案是将所有允许值设为联合类型:

type Values<T> = T[keyof T]

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

现在它按预期工作了:

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

更新

我重载了 commit 函数。

关于状态突变: 一般来说,TS 不能很好地处理突变。 因为对象与其键是逆变的,所以 string|boolean 被评估为 never,因为 string & boolean = never.

请参阅我的 article 关于打字稿中的突变。

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 
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'>;