Typescript error: The 'this' context of type ... is not assignable to method's 'this' of type

Viewed 3375

I'm currently debugging some libraries' code and for the life of me don't understand a specific error.

Here is a simplified example of the code I'm debugging:

type Test1Type<V> = object | ((this: V) => object);

function testFnc(x: string, test1Fnc: Test1Type<string>) {
  let res;

  if (typeof test1Fnc === "function") {
    res = test1Fnc.call(x, x);
  }
}

In the line res = test1Fnc.call(x, x); the term test1Fnc is marked as an error:

(parameter) test1Fnc: Function | ((this: string) => object)

The 'this' context of type 'Function | ((this: string) => object)' is not assignable to method's 'this' of type '((this: string) => object) & Function'.
  Type 'Function' is not assignable to type '((this: string) => object) & Function'.
    Type 'Function' is not assignable to type '(this: string) => object'.
      Type 'Function' provides no match for the signature '(this: string): object'.ts(2684)

The internal type definition of .call() seems to be this:

call<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, ...args: A): R;

Unfortunately, I just cannot figure out what is wrong with this code, especially since the library I'm debugging has this directly in their codebase (see).

I don't really understand what the error message is telling me here.

I'm using "typescript": "~3.9.3" (the library is using "typescript": "^2.8.3").

Any help is appreciated, since I would really like to understand what is going on here.

2 Answers

You have found an inconsistency in the way TypeScript deals with the interaction of Function and union types.

What the compiler does

When call is called, the compiler knows test1Fnc is either:

  1. a JavaScript Function object of some unknown signature, or
  2. a JavaScript Function object which is called on a string (ie it has a string as its this argument) and returns an object.

The compiler then asks if the call is valid for each of those types.

When it asks if the call is valid for type 1, it concludes that the call is not because test1Fnc may not be callable on a string.

Why this is odd

This is odd because this is contrary to regular TypeScript behaviour for Function objects, where it normally allows any kind of function call. To see that such calls are allowed, temporarily change the initial type declaration to type Test1Type<V> = object and the error disappears.

This happens because two things about how Function types are processed by the compiler are true:

  1. Any kind of call of a function on them is permissible, but
  2. They are not assignable to a function with specified arguments.

When the compiler knows test1Fnc is a Function only (as when the type declaration is changed), it works because of rule 1. But when the union type is involved, it first tries to assign the union type to the given signature of the call type. That assignment fails because of rule 2 before rule 1 has had a chance to override it.

Why this is not how the compiler should behave

You can see this is not the intended behaviour because if Test1Type<V> is defined to be just either one of those two types (provided the modification explained in the next section is made) then the compiler does not give any errors. This is not right: if a path through the program is valid for either type in a union type, then the program should be valid with the union type.

Yet the type is wrong anyway

As another observation, even if test1Fnc is type 2 from above, it would not not be callable with the call function set out. The given call function expects a this value of string and also one regular argument of type string, yet type 2 from above is just a this value of type string with no regular arguments. The compiler is not showing this error at present, but if you change the initial type declaration to type Test1Type<V> = ((this: V) => object) then you will see a different error produced. In turn this is eliminated by changing the line to type Test1Type<V> = ((this: V, arg: V) => object).

Suggestions: redefine, cast, report and don't use unions

To deal with this, you might consider one or more of the following:

  1. Redefine Test1Type<V> = object | ((this: V, arg: V) => object), which appears to be what the code is expecting. This doesn't really change the type given it could still be any object, but it does express the code's intention better
  2. Then to clear the error, if you can cast the type of test1Fnc within the if block to the expected type: res = (test1Fnc as (this: string, arg: string) => object).call(x,x). If you are unable to make a change like this, you might have to give up on any type safety and go for type Test1Type = object
  3. You might like to report this an issue on the TypeScript GitHub. I'm not sure it's a priority for them as the use of Function objects for typing are probably not encouraged, and
  4. I'd discourage the use of this union type anyhow. It's so general it's not really achieving type safety in any case. It's not clear how much control you have over the code, and what other values Test1Type<V> might need to be, but if you can make wider changes, it would be preferable to write it as a union of more explicit types so the compiler could choose between them, or more rigorously set up a class hierarchy in full OOP style.

I ran into this with RxJS and Highcharts. It was in a subscription waiting for the chart to be created.

Like:

chartCreation.subscribe(() => {
      this.chart.events.redraw({ ...config... });
})

the solution was to bind chart.events to "this" so it would see them as the same type.

chartCreation.subscribe(() => {
      this.chart.events.redraw.bind(this)({ ...config... });
})
Related