我有以下类(class)DataStore
具有 Map 类型的两个属性。我正在尝试想出一个方法getEntity
它将根据参数返回正确的数据类型。还尝试创建一个方法updateEntity
用于更新属性。
type TUUID = string;
interface IUser {
name: string;
type: 'customer' | 'client';
}
interface IProduct {
name: string;
count: number;
type: 'electronics' | 'food' | 'drinks';
}
const _user: IUser = { name: 'John Doe', type: 'customer' };
interface IDataStore {
users: Map<TUUID, IUser>;
products: Map<TUUID, IProduct>;
}
export class DataStore implements IDataStore {
users: IDataStore['users'] = new Map();
products: IDataStore['products'] = new Map();
getEntity<T extends keyof IDataStore>(storeKey: T, id: TUUID) {
if (this[storeKey] && this[storeKey].has(id)) return this[storeKey].get(id);
return null;
}
updateEntity<T extends keyof IDataStore>(storeKey: T, id: TUUID, data: ReturnType<IDataStore[T]['get']>) {
if (this[storeKey]) {
this[storeKey].set(id, data);
return this[storeKey].get(id);
} else {
return null;
}
}
}
const store = new DataStore();
const user = store.getEntity('users', '1234');
const product = store.getEntity('products', 'abcd');
const updatedUser = store.updateEntity('users', '1234', _user);
但是 typescript 推断出 getEntity
的数据类型如IUser | IProduct | null | undefined
。当尝试访问product.count
时,它提供以下错误:
Property 'count' does not exist on type 'IUser | IProduct'.
Property 'count' does not exist on type 'IUser'.
此外,这一行 this[storeKey].set(id, data);
提供错误:
(parameter) data: IUser | IProduct | undefined
Argument of type 'IUser | IProduct | undefined' is not assignable to parameter of type 'never'.
The intersection 'IUser & IProduct' was reduced to 'never' because property 'type' has conflicting types in some constituents.
Type 'undefined' is not assignable to type 'never'.ts(2345)
如果我进行方法重载,它基本上可以工作。但随后方法内部的方法参数就变成any
这很容易出错。我尝试使用 Record 并且只是纯对象,仍然有同样的问题。但是,如果我也为特定属性执行方法,如 getUser
, getProduct
, updateUser
和updateProduct
它确实推断出了正确的类型。
有没有办法拥有一个能够正确推断数据类型的通用方法?
最佳答案
首先,为了this[storeKey].set(id, data)
要工作,您需要重构 this
的类型和data
为了让编译器将它们视为明显彼此兼容。类似 this[storeKey].set()
的一般问题和data
关联union types microsoft/TypeScript#30581 中描述,以及 microsoft/TypeScript#47109 中实现和描述的修复是重构为分布式对象类型。
例如,这里有一种重写 IDataStore
的方法:
interface DataMap {
users: IUser;
products: IProduct;
}
type IDataStore = { [K in keyof DataMap]: Map<TUUID, DataMap[K]> }
这看起来像一个无操作,事实上,如果你检查IDataStore
,看起来和以前一样:
/*type IDataStore = {
users: Map<TUUID, IUser>;
products: Map<TUUID, IProduct>;
}*/
但这里重要的是编译器知道 IDataStore
一般作用于 K extends keyof DataMap
以某种方式它不知道你原来的定义。在 updateEntity
我们还更改 data
的类型至DataMap[K]
而不是ReturnType<IDataStore[K]['get']>
。前者被视为与 IDataStore
有关。通用定义 K
,而后者太复杂并且使用 the ReturnType<T>
utility type它被实现为 conditional type .
这就是我们到目前为止所得到的:
getEntity<K extends keyof IDataStore>(storeKey: K, id: TUUID) {
return this[storeKey].get(id) ?? null;
} // returns IUser | IProduct | null
updateEntity<K extends keyof IDataStore>(storeKey: K, id: TUUID, data: DataMap[K]) {
this[storeKey].set(id, data); // error! Argument of type
// 'IUser | IProduct' is not assignable to parameter of type 'never'
return data;
}
(请注意,我删除了您的存在/不存在测试,因为您的属性不是可选的)。这显然不是一个进步; getEntity()
的输出类型仍然是一个联盟,并且 set()
方法仍然提示交集。现在的问题是 this
的类型是polymorphic this
type编译器将其视为隐式 generic类型参数。它自动表示实现子类的类型。这会让编译器感到困惑,它无法看到与 IDataStore
的连接。 。编译器 View this[storeKey]
作为联合类型,因为它只是加宽 this
到其DataStore
类(Class)约束。我们希望按照 IDataStore
开展工作这是专门为维护相关性而构建的。
我在这里的方法是安全地扩大 this
至IDataStore
在访问storeKey
之前属性:
getEntity<K extends keyof IDataStore>(storeKey: K, id: TUUID) {
const thiz: IDataStore = this;
return thiz[storeKey].get(id) ?? null;
} // returns NonNullable<DataMap[K]> | null
updateEntity<K extends keyof IDataStore>(storeKey: K, id: TUUID, data: DataMap[K]) {
const thiz: IDataStore = this;
thiz[storeKey].set(id, data); // okay
}
现在一切正常了。 thiz[storeKey]
的类型现在以通用 K
表示输入参数,这样 get()
返回DataMap[K] | undefined
和set()
接受DataMap[K]
.
关于Typescript:如何从具有映射/记录类型值的类属性返回特定类型?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72587596/