Your main issue arises from the fact that your type parameter O
has a default type argument but lacks constraints to that type. Additionally, O
is not referenced anywhere in the function parameters, resulting in no inference site for it.
It is preferable to have minimal type parameters and to directly include them in your function parameters whenever possible. When defining a type parameter X
, it should ideally be inferred from a parameter of type X
(e.g., <X>(x: X) => void
), or from a property of type X
(e.g., <X>(v: {v: X}) => void
), or from a homomorphic mapped type on X
. The goal is to make the type parameters as explicit as possible.
To address this, consider revising the call signature of inject()
as shown below:
function inject<
K extends PropertyKey,
T extends Record<K, (...args: any) => any>
>(
o: { prototype: T },
func_name: K,
func: (ret: ReturnType<T[K]>) => void
) { }
In this revision, K
represents the type of func_name
, constrained as a string literal due to its constraint of
PropertyKey</code. Meanwhile, <code>T
signifies the type of the
prototype
property of
o
, with the additional constraint of having a function property at key
K
.
Subsequently, the callback function func
expects an argument of ReturnType<T[K]>
. This contextual typing ensures seamless interaction with the already-inferred types T
and
K</code. </p>
<p>Testing the revised implementation:</p>
<pre><code>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'
The compiler accurately handles valid calls while flagging incompatible ones based on type discrepancies within the provided arguments.
Link to code snippet