Bluebird / Typescript Error: 'this' context of is not assignable to method's 'this'

580 views Asked by At

I have a series of methods (some asynchronous and some not) that I would like to use bluebird.each to process in order. Here is a dumbed down example:

import bluebird from 'bluebird';

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
class Template {
  propertyA!: number;
  propertyB!: number;
  constructor() {}
  async methodA() {
    this.propertyA = 10;
    console.log({ thisArgInA: this, propertyA: this.propertyA });
    await delay(500);
    return this.propertyA;
  }
  methodB() {
    this.propertyB = this.propertyA * 100;
    console.log({ thisArgInB: this, propertyB: this.propertyB });
    return this.propertyB;
  }
}

const instance = new Template();

const sequence = [instance.methodA, instance.methodB];

(async function main() {
  await bluebird.each(sequence, (fn) => {
    const result = fn.call(instance);
    console.log({ result });
  });
})();

This produces this error I do not understand:

index.ts:27:20 - error TS2684: The 'this' context of type '(() => Promise<number>) | (() => number)' is not assignable to method's 'this' of type '(this: Template) => Promise<number>'.
  Type '() => number' is not assignable to type '(this: Template) => Promise<number>'.
    Type 'number' is not assignable to type 'Promise<number>'.

27     const result = fn.call(instance);

I thought maybe it is because bluebird.each accepts either values or promise that resolve to values the compiler can't reconcile things, but the only solution I thought of there was to strongly type the return values"

async methodA: Promise<number> { /*...*/ }
methodB: number { /*...*/ }

But that didn't change anything. Also I note the following:

  • I have to provide context here, because invoking the functions in the each callbacks loses connection to the Template as this
  • When methodA is synchronous (remove the async keyword, remove the delay(), just return this.propertyA, this works fine.
  • When I remove the return statements things work fine.
  • When I change fn.call(instance) to fn.bind(instance)() it works fine

How can I satisfy the compiler so it knows how to call these functions with the provided context.

Is there an easier way to call these methods in sequence so their connection to this is maintained?

StackBlitz Example

1

There are 1 answers

2
Bergi On

It may help to look at the inferred types:

  • sequence: (((this: Template) => number) | ((this: Template) => Promise<number>))[] or just ((() => Promise<number>) | (() => number))[]
  • fn: ((this: Template) => number) | ((this: Template) => Promise<number>) or just (() => Promise<number>) | (() => number)
  • .call: <Template, [], number>(this: (this: Template) => number, thisArg: Template): number

and we can see that the type of the .call() expression is not what we expect - the compiler wants a function returning a number, and complains since fn can alternatively return a Promise<number>. This is probably due to a TypeScript bug when dealing with inferring the return type of function union types. It might be related to the subtle difference between X => A | B and (X => A) | (X => B).

We can fix this by writing

  • const result = fn.call<Template,[],number|Promise<number>>(instance); or
  • …(fn: () => number | Promise<number>)) => … or
  • const sequence: (() => number | Promise<number>)[]