为什么“keyof”对类构造函数返回的 Proxy() 内的泛型类型不起作用?

问题描述 投票:0回答:2

我在将泛型类型与

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

typescript typescript-generics
2个回答
3
投票

假设

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 拒绝相信

1
投票
可以分配给

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
文件。也许其他人能够插话并为您提供更多可行的建议。

© www.soinside.com 2019 - 2024. All rights reserved.