Following on from this question, I am now trying to create functions with an explicit this parameter of a type extending interface IModel
:
// interface for Model class
interface IModel {
state: {}
}
// based on answer to this question https://stackoverflow.com/q/59895071/374328
// now functions will be bound to model instance, so need to specify 'this' parameter as Model
type SingleArgFunction<Model extends IModel, A> = (this: Model, x: A) => A;
type ArrayedReturnFunction<Model extends IModel, A> = (this: Model, x: A) => A[];
type SingleArgFunctionObject<Model extends IModel, AS extends object> = {
[K in keyof AS]: SingleArgFunction<Model, AS[K]>
}
type ArrayedReturnFunctionObject<Model extends IModel, AS extends object> = {
[K in keyof AS]: ArrayedReturnFunction<Model, AS[K]>
}
function makeArrayed<Model extends IModel, A>(f: SingleArgFunction<Model, A>): ArrayedReturnFunction<Model, A> {
return function (x) {
return [f.call(this, x)];
}
}
function makeArrayedAll<Model extends IModel, AS extends object>(
fs: SingleArgFunctionObject<Model, AS>
): ArrayedReturnFunctionObject<Model, AS> {
const result = {} as ArrayedReturnFunctionObject<Model, AS>;
(Object.keys(fs) as (keyof AS)[]).forEach(function<K extends keyof AS>(key: K) {
result[key] = makeArrayed(fs[key]);
})
return result;
}
An example model and type definitions:
interface MyModel extends IModel {
state: {
x: number;
}
}
interface SingleArgFunctions {
foo: SingleArgFunction<MyModel, number>
}
interface ArrayedReturnFunctions {
foo: ArrayedReturnFunction<MyModel, number>;
}
Creating an object of ArrayedReturnFunctions
directly is ok, with this
being inferred as type MyModel
:
const arrayedReturnFunctions1: ArrayedReturnFunctions = {
foo(x) {
return [x + this.state.x]; // ok
}
}
Alternatively, creating the object by applying makeArrayed
to a single function is also ok:
const arrayedReturnFunctions2: ArrayedReturnFunctions = {
foo: makeArrayed(function (x) {
return x + this.state.x; // ok
})
}
However, using makeArrayedAll
does not work - this
is inferred as type IModel
:
const arrayedReturnFunctions3: ArrayedReturnFunctions = makeArrayedAll({
foo(x) {
return x + this.state.x; // error - property x does not exist on type {}
}
})
Even creating an object of type SingleArgFunctions
and then passing that to makeArrayedAll
does not work:
const singleArgFunctions: SingleArgFunctions = {
foo(x) {
return this.state.x + x;
}
}
const arrayedReturnFunctions4 = makeArrayedAll(singleArgFunctions); // error - IModel is not assignable to type MyModel
Why is the Model
type not inferred as MyModel
when using makeArrayedAll
?
To me this looks like you're hoping the type of the variable arrayedReturnFunctions
to which you're assigning the output of makeArrayedAll()
will provide enough contextual typing for the input of makeArrayedAll()
so that the input methods' this
context will be inferred... but it's not happening. I'm not exactly surprised that contextual inference would fail to happen in this scenario but I don't have a great answer about exactly why or how to force it to happen.
Right now my only suggestion is to note that type inference tends to work better in the "forward" direction; that is, a generic function's type parameters are easier to infer from the input to that function and not from the expected output type of the function. And if that fails, you could always manually specify the generic type parameter.
Here's one way to manually specify the model type parameter and let the compiler infer the rest, using currying:
const makeArrayedAllFor = <M extends IModel>() => <AS extends object>(
fs: SingleArgFunctionObject<M, AS>) => makeArrayedAll(fs);
const arrayedReturnFunctions3: ArrayedReturnFunctions = makeArrayedAllFor<MyModel>()({
foo(x) {
return x + this.state.x;
}
})
const arrayedReturnFunctions4 = makeArrayedAllFor<MyModel>()(singleArgFunctions);
This works now, although it's cumbersome. That's the best I can think of at the moment; maybe someone has other ideas? Oh well, good luck!