typescript - 如何在 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 组合的要求是否太多?
解决方案
有时很难使用所有这些泛型。
在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'>;
推荐阅读
- excel - 运行此代码行时出错。请评论
- html - 单击浏览器的后退按钮时清除单选和复选框按钮状态
- powershell - PowerShell 扫描注册表对象并进行比较
- google-cloud-data-fusion - 如何在 Cloud Data Fusion 中合并多个具有相同结构的 CSV 文件?
- angular - npm-install 命令在 Angular 7 项目中不起作用
- python - 如何将单个数据框列转换为以列名作为键的每一行的字典?
- postgresql - PostgreSQL 全文搜索从选择中删除行
- c# - 任务中的异常不会导致应用程序崩溃
- typescript - 如何为泛型静态方法正确应用泛型类型定义?
- php - 在 Laravel 中使用 PHP $_COOKIE 访问 Cookie。是还是不是?