1

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?

Jin
  • 53
  • 1
  • 6
  • 1
    I hereby perform a summoning ritual for @jcalz and captain-yossarian. Though personally I think the answer by Lioness100 is correct. – CoderApprentice Feb 14 '23 at 15:08
  • The answer below is essentially correct; you are just assigning a [default type argument](//www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults) to `O`, not constraining it in any way. Furthermore it's rarely useful for generic type parameters not to directly appear in function parameters, so `O` is suspect anyway. I'd expect `inject()` to look like [this](//tsplay.dev/NnlbaW) instead. But you're not quite asking that question (well, maybe "should I write my code differently"?), so I don't know if it's worth posting my own answer. Thoughts? – jcalz Feb 14 '23 at 21:57
  • Your code looks more elegant, I just want to know the best practice to implement my idea. Can edit my question if needed.@jcalz – Jin Feb 16 '23 at 05:13

2 Answers2

1

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.

Lioness100
  • 8,260
  • 6
  • 18
  • 49
0

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 What does "homomorphic mapped type" mean? 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).

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360