我在将泛型类型与
keyof
中的 Proxy()
结合使用时遇到问题:
下面的示例代码不起作用,并抱怨没有任何可分配的类型:
interface SomeDataStructure {
name?:string;
}
class DataWrapper<T extends SomeDataStructure> {
data:T;
constructor( data:T ) {
this.data = data;
return new Proxy( this, {
get: ( target:this, property:keyof T ) : any => {
return target.data[ property ];
}
} );
}
}
错误(
get
):
Type '(target: this, property: keyof T) => any' is not assignable to type '(target: this, p:
string | symbol, receiver: any) => any'.
Types of parameters 'property' and 'p' are incompatible.
Type 'string | symbol' is not assignable to type 'keyof T'.
Type 'string' is not assignable to type 'keyof T'.
Type 'string' is not assignable to type '"name"'.ts(2322)
如果我直接使用
keyof SomeDataStructure
,而不是keyof T
,错误就会消失,一切看起来都很好:
interface SomeDataStructure {
name?:string;
}
class DataWrapper<T extends SomeDataStructure> {
data:T;
constructor( data:T ) {
this.data = data;
return new Proxy( this, {
get: ( target:this, property:keyof SomeDataStructure ) : any => {
return target.data[ property ];
}
} );
}
}
或者,写
keyof T extends SomeDataStructure
(就像在类定义中一样)也可以。一般来说,在 keyof T
之外的类方法中使用 Proxy()
也可以很好地工作。
我必须假设当将东西包裹在
T
中时,new Proxy()
的类型会以某种方式丢失。我如何在这里重新使用 T
来代替 keyof T
?
假设
property
属于 keyof T
类型并不安全,但 TypeScript 通常允许 get 处理程序对 property
做出不安全的假设。您收到错误的原因是因为 keyof T
是不安全的,这会让编译器感到困惑。
首先:假设
property
是 keyof T
类型是不安全的。
您的 generic 类型参数
T
只是 constrained 到 SomeDataStructure
,这意味着它可能是 SomeDataStructure
的 子类型,其属性比
SomeDataStructure
所了解的更多:
const dw = new DataWrapper({
name: "abc",
otherProp: 123,
4: true,
}); // compiles okay
此外,实际传入的
data
可能是 T
的 子类型,其属性甚至在 T
中都不为人知:
interface Foo extends SomeDataStructure {
name: string
otherProp: number,
4: boolean
}
const bar = { name: "abc", otherProp: 123, 4: true, yetAnotherProp: "hello" };
const foo: Foo = bar; // okay
const dw2 = new DataWrapper(foo);
// const dw2: DataWrapper<Foo>
这里
dw2
属于 DataWrapper<Foo>
类型,但实际数据具有 yetAnotherProp
中未知的 Foo
属性。这正是 TypeScript 中结构类型的工作方式,而且是有意为之的。
因此从技术上讲,您的代理处理程序应该准备好接收任何属性密钥,而不仅仅是
keyof T
。并且 ProxyHandler
的 TypeScript get
打字表示 property
参数相应地应为
string | symbol
类型。但由于它是使用
方法语法 ({ get(⋯): ⋯ }
) 而不是函数属性语法 (
{ get: (⋯) => ⋯ }
) 进行声明,TypeScript 在其参数类型中将其视为bivariant,这意味着它将接受在安全版本中,您将
property
注释为比 string | symbol
更宽,而且在 不安全但方便版本中,您将
property
注释为比 string | symbol
更窄。这意味着,如果您将 keyof T
更改为
"bloop"
,错误就会消失,因为即使假设 property
将是 "bloop"
也是不安全的,但它比 string | symbol
窄,因此允许:new Proxy(this, {
get: (target: this, property: "bloop"): any => { // okay!
return "";
}
});
对您来说不幸的是,
keyof T
既不
比
string | symbol
更宽也不更窄。类型 keyof T
允许包含
number
元素,如上所示的
4
。当然,实际上,这些键被强制为
string
,并且该键实际上是
"4"
。但在某些地方,TypeScript 假装键可以是
number
,主要是为了允许使用数字对arrays
进行索引,因此存在“数字”索引签名。 并且
property
不能是 number
,因为microsoft/TypeScript#35594
进行了更改,以修复microsoft/TypeScript#39272 中报告的错误,即
number
永远不会传递给处理程序。因此他们将其从 string | number | symbol
(
keyof T
比那个窄)更改为
string | symbol
(
keyof T
不比那个窄)。
所以你会得到一个错误。
keyof T
是不安全的,但不是编译器允许的方式。你能做什么来解决这个问题? “正确”的事情是编写可以处理任何
string | symbol
Exclude
中选择
number
keyof T
,这样它就会被视为更窄并且双方差开始:
interface SomeDataStructure {
name?: string;
}
class DataWrapper<T extends SomeDataStructure> {
data: T;
constructor(data: T) {
this.data = data;
return new Proxy(this, {
get: (target: this, property: Exclude<keyof T, number>): any => { // okay
return target.data[property];
}
});
}
}
现在可以了。Playground 代码链接
您收到的消息是正确的:TypeScript 拒绝相信keyof T
。这是很合理的,它们是两种不同的类型。 (“可分配”意味着
string | symbol
必须等于或小于
keyof T
- 这显然是不正确的。)
无法告诉 TypeScript 事实上,它应该总是期望
keyof T
被传递到那里,这是由于 ProxyHandler
恕我直言,类型错误造成的。您可以通过单击编辑器中的
get
函数并转到其类型定义来观察这一点。
目前输入的是:
get?(target: T, p: string | symbol, receiver: any): any;
虽然你想要的是:
get?<K = keyof T>(target: T, p: K, receiver: any): T[K];
第二次打字,你就能更精准地与TS沟通了:
get: <K extends keyof T>(target: this, property: K): T[K] => {
const value = target.data[ property ];
return value; // correctly evaluates to `T[K]`
}
如果您只需编辑
lib.es20XX.proxy.d.ts
文件并在其中添加替代类型,就可以在编辑器中对此进行测试:
/**
* A trap for getting a property value.
* @param target The original object which is being proxied.
* @param p The name or `Symbol` of the property to get.
* @param receiver The proxy or an object that inherits from the proxy.
*/
get?(target: T, p: string | symbol, receiver: any): any;
get?<K = keyof T>(target: T, p: K, receiver: any): T[K];
我很遗憾地说我的知识仅限于此。我不确定是否有一个简单的内联解决方案可以让您缩小本机输入范围,而无需添加自己的 .d.ts
文件。也许其他人能够插话并为您提供更多可行的建议。