英文:
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<
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?
答案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<
T,
O extends Record<any, (...args: any) => any> = {
[K in keyof T as T[K] extends (...args: any) => any ? K : never]: T[K] extends (...args: any) => any ? T[K] : never;
},
K extends keyof O = keyof O,
F extends (...args: any) => any = O[K]
>(o: { prototype: T }, func_name: K, func: (ret: ReturnType<F>) => 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) => 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<any, (...args: any) => any>
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
) { }
在这里,K
是func_name
的类型,并且可以推断为func_name
参数的字符串字面类型,因为它受限于PropertyKey
,而PropertyKey
是string | number | symbol
。T
是o
的prototype
属性的类型,其中T
受限于具有键为 K
的函数属性(使用Record<K, V> 实用类型)。因此,T
将从o
参数的prototype
属性中推断出来,如果o.prototype[func_name]
不是函数类型,将会报错。
最后,func
的类型是一个回调,其参数是ReturnType<T[K]>
,使用ReturnTypeT
或K
(它太复杂了)...但没关系,因为T
和K
应该已经从其他参数中推断出来。相反,编译器将使用具有已推断的T
和K
的ReturnType<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 将原型模型化为与类实例相同,无论好坏如何,所以编译器无法捕获这个问题)。
英文:
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., <X,>(x: X) => void
); it can also be inferred from a parameter with a property of type X
(e.g., <X,>(v: {v: X}) => void
, or from a homomorphic mapped type on X
(e.g., <X,>(v: {[K in keyof X]: ...X[K]...}) => 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<
K extends PropertyKey,
T extends Record<K, (...args: any) => any>
>(
o: { prototype: T },
func_name: K,
func: (ret: ReturnType<T[K]>) => 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<K, V>
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<T[K]>
using the ReturnType<T>
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<T[K]>
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 "xyz"
}
qux = 10
}
inject(Foo, "bar", x => x / 2);
inject(Foo, "bar", x => x.toUpperCase()); // error!
// Property 'toUpperCase' does not exist on type 'number'
inject(Foo, "baz", x => x.toUpperCase());
inject(Foo, "qux", x => x) // error!
// Type 'number' is not assignable to type '(...args: any) => any'
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).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论