Typescript ‘method of’ operator

huangapple go评论58阅读模式
英文:

Typescript 'method of' operator

问题

I want to write a function that modifies a method on a function prototype, like this:

function inject<T, O = {
  [K in keyof T as T[K] extends (...args: any) => any ? K : never]: T[K];
},
K extends keyof O = keyof O,
F extends (...args: any) => any = O[K]    // error
>(o: { prototype: T }, func_name: K, func: (ret: ReturnType<F>) => void) {}

But TypeScript reported an error saying "type 'O[K]' does not satisfy the constraint '(...args: any) => any'".

How do I fix this, or should I write my code differently?

英文:

I want to write a function that modifies a method on a function prototype, like this:

function inject&lt;
  T,
  O = {
    [K in keyof T as T[K] extends (...args: any) =&gt; any ? K : never]: T[K];
  },
  K extends keyof O = keyof O,
  F extends (...args: any) =&gt; any = O[K]    // error
&gt;(o: { prototype: T }, func_name: K, func: (ret: ReturnType&lt;F&gt;) =&gt; void) {}

But typescript reported an error saying type &quot;O[K]&quot; does not satisfy the constraint &quot;(...args: any) =&gt; any&quot;.

How do I fix this, or should I write my code differently?

答案1

得分: 1

这段代码似乎在讨论如何解决一个问题,问题出在泛型 O 的定义上,因为 TypeScript 无法确定 O[K] 是否为一个函数。为了解决这个问题,建议将 O 扩展为具有函数值的对象(就像 K 扩展为 keyof O 并赋给 keyof O 一样)。T[K] extends (...args: any) => any ? T[K] : never 的部分是必要的,因为 TypeScript 需要明确知道这个值是一个函数,以满足 Record<any, (...args: any) => any> 的签名要求。

英文:

I don't know if this is the best or easiest way to do it, but I suppressed the error by changing the O generic.

function inject&lt;
  T,
  O extends Record&lt;any, (...args: any) =&gt; any&gt; = {
    [K in keyof T as T[K] extends (...args: any) =&gt; any ? K : never]: T[K] extends (...args: any) =&gt; any ? T[K] : never;
  },
  K extends keyof O = keyof O,
  F extends (...args: any) =&gt; any = O[K]
&gt;(o: { prototype: T }, func_name: K, func: (ret: ReturnType&lt;F&gt;) =&gt; void) {}

Part of the problem is that O could be an arbitrary value, so typescript has no reason to believe that O[K] will be a function. To fix this, O should extend an object with function values (just like K extends keyof O and is assigned to keyof O). The T[K] extends (...args: any) =&gt; any ? T[K] : never is necessary because I think typescript isn't smart enough to infer that the value has to be a function due to the never guard in the value. It needs to know that it's a function so it can fit the Record&lt;any, (...args: any) =&gt; any&gt; signature.

答案2

得分: 0

以下是您要翻译的内容的翻译部分:

主要问题是,您的 O 类型参数具有默认类型参数,但未被约束到该类型,并且无论如何,O 在函数参数的类型中都没有出现,因此没有推断点。理想情况下,您希望尽可能少地使用类型参数,并希望它们尽可能直接地出现在函数参数中。如果您有一个类型参数 X,那么它最容易从类型为 X 的参数中推断出来(例如,<X,>(x: X) => void);它还可以从具有类型 X 的属性的参数中推断出来(例如,<X,>(v: {v: X}) => void),或者从 X 上的同态映射类型中推断出来(例如,<X,>(v: {[K in keyof X]: ...X[K]...}) => void,请参阅https://stackoverflow.com/q/59790508/2887218获取更多信息)。并且有时也可以使用更复杂的东西进行推断,但一般规则是希望它尽可能直接出现。

因此,我可能会将 inject() 的调用签名写成如下形式:

function inject<
    K extends PropertyKey,
    T extends Record<K, (...args: any) => any>
>(
    o: { prototype: T },
    func_name: K,
    func: (ret: ReturnType<T[K]>) => void
) { }

在这里,Kfunc_name的类型,并且可以推断为func_name参数的字符串字面类型,因为它受限于PropertyKey,而PropertyKeystring | number | symbolToprototype属性的类型,其中T 受限于具有键为 K 的函数属性(使用Record<K, V> 实用类型)。因此,T 将从o 参数的prototype属性中推断出来,如果o.prototype[func_name]不是函数类型,将会报错。

最后,func 的类型是一个回调,其参数是ReturnType<T[K]>,使用ReturnType 实用类型。这不会用于推断TK(它太复杂了)...但没关系,因为TK应该已经从其他参数中推断出来。相反,编译器将使用具有已推断的TKReturnType<T[K]>上下文类型化回调参数。也就是说,这里的推断是反过来的。让我们来测试一下:

class Foo {
    bar() {
        return 123;
    }
    baz() {
        return "xyz";
    }
    qux = 10;
}

inject(Foo, "bar", x => x / 2);
inject(Foo, "bar", x => x.toUpperCase()); // 错误!
// 类型“number”上不存在属性“toUpperCase”
inject(Foo, "baz", x => x.toUpperCase());
inject(Foo, "qux", x => x); // 错误!
// 类型“number”不能赋值给类型“(...args: any) => any”

看起来不错。编译器对第一个调用感到满意,并理解x的类型是number,因为Foo.prototype.bar是返回number的函数。第二个调用会报错,因为number没有toUpperCase。然后第三个调用是可以的,因为new Foo().baz()返回一个string。第四个调用失败,因为new Foo().qux根本不是函数;它是一个number(而且它根本不会在原型上,但 TypeScript 将原型模型化为与类实例相同,无论好坏如何,所以编译器无法捕获这个问题)。

[代码示例的Playground链接](https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABDMArAptAPAKEfxAaUXQA8p0wATAZ0QAUAnOAB3UagE9D1OAaPAQAqJcpVqIASpjiMqWQn0QAKAHTqAhowDmNAFyINYTgEpEAXgB8h45ZyXlg-HAMBvRC2ZQ4XNgZEAvgIEiKCQAPpgGgC26AaKTqHgEAbKjOhQBtJQIIxgQpxsWEIA2oQAupZmVogAbnAwVDhm7gE4OBAANho0dABicHCIrokARlrKLYkh6Tl5iACMAEwAzADciW0h4wBek8PTBLO5SABEpJw7p5uJAI4gpBaLAAw4bTgoGNDKA3BKp+NGKclI8ao8APSIJYmDafTBQH6Df6A4GIUHWUiqbwAVRYbEYAGEeuhJjDEODIexmIwAIQMZj4riIADkOLx7CJNHQzMQVDg6DoYB8ohgNCgiAQiF86BZYBA0VG7GZHzQ8MRf0QAI0VxBFgxWLguPxnJJJhhKq+CN+-3upFR6LRZgpJEY1LpBTYsvlisYPNFiCF4p6NBg2iio06Mu8UsKMuZak0On0NlMepTz

英文:

Your main problem is that your O type parameter has a default type argument but is not constrained to that type, and in any case, O does not appear anywhere in the types of your function parameters, so there is no inference site for it.

Ideally you want as few type parameters as possible and you want them to appear in your function parameters as directly as possible. If you have a type parameter X then it is most easily inferred from a parameter of type X (e.g., &lt;X,&gt;(x: X) =&gt; void); it can also be inferred from a parameter with a property of type X (e.g., &lt;X,&gt;(v: {v: X}) =&gt; void, or from a homomorphic mapped type on X (e.g., &lt;X,&gt;(v: {[K in keyof X]: ...X[K]...}) =&gt; void, see https://stackoverflow.com/q/59790508/2887218 for more info). And more complicated things can also sometimes be used in inference, but the general rule is you want it to appear as directly as possible.

So I'd probably write inject()'s call signature as follows:

function inject&lt;
    K extends PropertyKey,
    T extends Record&lt;K, (...args: any) =&gt; any&gt;
&gt;(
    o: { prototype: T },
    func_name: K,
    func: (ret: ReturnType&lt;T[K]&gt;) =&gt; void
) { }

Here K is the type of func_name, and can be inferred to be the string literal type of the func_name argument because it is constrained to PropertyKey which is string | number | symbol. And T is the type of the prototype property of o , where T is constrained to have a function property at key K (using the Record&lt;K, V&gt; utility type). So T will be inferred from the prototype property of the o argument, and there will be an error if o.prototype[func_name] isn't of a function type.

Finally, the type of func is a callback whose argument is ReturnType&lt;T[K]&gt; using the ReturnType&lt;T&gt; utility type. This will not be used to infer T or K (it's too complicated)... but that's okay because T and K should already be inferred from other arguments. Instead, the compiler will use ReturnType&lt;T[K]&gt; with the already-inferred T and K to contextually type the callback argument. That is, inference is going the other way here. Let's test it out:

class Foo {
    bar() {
        return 123;
    }
    baz() {
        return &quot;xyz&quot;
    }
    qux = 10
}

inject(Foo, &quot;bar&quot;, x =&gt; x / 2);
inject(Foo, &quot;bar&quot;, x =&gt; x.toUpperCase()); // error! 
// Property &#39;toUpperCase&#39; does not exist on type &#39;number&#39;
inject(Foo, &quot;baz&quot;, x =&gt; x.toUpperCase());
inject(Foo, &quot;qux&quot;, x =&gt; x) // error! 
// Type &#39;number&#39; is not assignable to type &#39;(...args: any) =&gt; any&#39;

Looks good. The compiler is happy about the first call, and it understands that x is of type number because Foo.prototype.bar is a number-returning function. The second call it complains because number doesn't have a toUpperCase. Then the third call is okay because new Foo().baz() returns a string. The fourth call fails because new Foo().qux isn't a function at all; it's a number (plus it won't be on the prototype at all, but TypeScript models prototypes as identical to class instances, for better or worse, so that's not something the compiler could catch).

Playground link to code

huangapple
  • 本文由 发表于 2023年2月14日 18:41:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/75446666.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定