首页 > 解决方案 > 如何在 vuex 存储打字稿中动态地设置对象的属性(同时保持类型安全)

问题描述

我有一个带有 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

这太棒了,它应该可以正常工作,但是如果我尝试将其应用于我在商店中拥有的突变,类型安全将不再存在,如果我尝试设置一个不存在的属性,它唯一会做的事情在类型顺序上,至少它告诉我该属性在类型 Order 上不存在。

这是我到目前为止的代码,当我将鼠标悬停在每个函数上时,我将包含屏幕截图以及它在 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 链接显示问题:

UPDATE2:一个迟缓的解决方案......(specialCommit ?????):

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

更新3:回应游乐场 所以基本上我会有不同种类的突变,不仅仅是我想要在这个问题的情况下工作的突变。您的回答揭示了背景中发生的事情,我对此表示感谢。也许你有别的想法?还是我应该做一些像我在智力迟钝的解决方案中所做的事情?基本上我的智障解决方案与您的类似,我发现参数部分的问题只是不知道为什么它不起作用。不过谢谢你的解释!另外,我对这个解决方案的另一个问题是,它将我对所有提交的使用限制为只有一种类型......也许我想设置 Order 的子对象的属性,这也将消除该选项。

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

标签: typescriptvue.jsvuex

解决方案


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

setMeta函数中,TS 能够推断类型,因为您调用了此函数。

问题出在这一行:

Parameters<Mutations[Key]>[1]

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

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

操场

更新

我重载了commit函数。

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

请参阅我关于打字稿突变的文章。

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

推荐阅读