If I wanted to create a function that takes an object of type T and another value, where the type P should somehow be restricted by T (for example, P should be an array of keys of T), I could easily write it like this:
function customFunction<T, P extends keyof T>(obj: T, p: P[]) {
// use p to index obj somehow
return obj;
}
customFunction({ a: 1, b: 'foo' }, ['a']); // Ok
customFunction({ a: 1, b: 'foo' }, ['a', 'b']); // Ok
customFunction({ a: 1, b: 'foo' }, ['a', 'b', 'c']); // Error: 'c' is not a valid key
Now, let's say I want to use this function as an argument for a higher-order method. This method should accept the function alongside a second parameter called arg
, and then just call it with this
and arg
:
class Indexed {
constructor(public a: number = 1) {}
public apply<P>(f: (obj: this, arg: P) => this, arg: P) {
return f(this, arg);
}
}
const result1 = new Indexed().apply(customFunction, ['a']); // Error: Type 'string' is not assignable to type '"a" | "apply"'
const result2 = new Indexed().apply(customFunction, ['wtf']); // The same error occurs here
However, when using customFunction
directly, everything works as expected:
customFunction(new Indexed(), ['a']); // Ok
customFunction(new Indexed(), ['wtf']); // Error, as expected
The main question is: how can we write the apply
method so that it will accept or reject arguments in the same way the customFunction
does?
Note that since we don't know the restrictions of customFunction
beforehand, we cannot restrict P
with the same bounds as in customFunction
.