Home > OS >  Typescript (seemingly) choosing the wrong overload
Typescript (seemingly) choosing the wrong overload

Time:08-04

I have a method like this:

class Foo {
    public runInWorker(cb: () => void): void;
    public runInWorker(cb: () => Promise<void>): void | Promise<void>;
    public runInWorker(cb: () => void | Promise<void>): void | Promise<void> {
        if (cluster.isPrimary ?? cluster.isMaster) {
            return;
        }

        return cb();
    }
}

What I want to do is to pass in a sync or async function and I want the overload to return the correct type (i.e. void for a sync function and Promise<void> for an async function).

This works fine for a sync callback:

// result is void
const result = new Foo().runInWorker(() => {})

But it fails with an async callback:

// result is void, but it should be Promise<void>
const result = new Foo().runInWorker(async () => {})

How do I fix this issue?

CodePudding user response:

I'm not sure precisely why, but a function returning a promise of void is type-convertible to a function returning void (I suppose because the return value of a function returning void should never be used, so a function A-which-returns-non-void with compatible parameter signature to function B is compatible with B if / even if B returns void). The opposite is not true:

const ok: () => void = async () => {};
const notOk: () => Promise<void> = () => {};

The TypeScript handbook section on function overloads says:

In order for the compiler to pick the correct type check, it follows a similar process to the underlying JavaScript. It looks at the overload list and, proceeding with the first overload, attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload. For this reason, it’s customary to order overloads from most specific to least specific.

As I mentioned at the beginning, your overload which takes a sync callback and returns void can accept a sync or async callback. The overload which takes an async callback can only accept an async callback and not a sync callback, which makes it the "more specific" overload.

So to fix your problem, I'm pretty sure you can just reorder your overloads as so:

class Foo {
    public runInWorker(cb: () => Promise<void>): void | Promise<void>;
    public runInWorker(cb: () => void): void;
    public runInWorker(cb: () => void | Promise<void>): void | Promise<void> {
        return cb();
        // ...

CodePudding user response:

Use generics instead

class Foo {
  public runInWorker<T>(cb: () => T): T {
    return cb();
  }
}

CodePudding user response:

First up, the problem is probably this line:

    public runInWorker(cb: () => Promise<void>): void | Promise<void>;

If you pass a function returning a promise, the return value should be a promise, so change it to

    public runInWorker(cb: () => Promise<void>): Promise<void>;

However, I would argue that the design is bad. You should either work with callbacks or promises, not both. I.e. it's fine to have a function that can take either a callback as input and return nothing - or take no callback as input, and return a promise. But callbacks that return promises is a strange mix.

So I could deal with this in this way

public run(cb: (err: Error, data: Data) => void): void;
public run(): Promise<Data>;
public run(cb?: (err: Error, data: Data) => void): void | Promise<Data> {
  // In this version, the function is natively implemented with callbacks,
  // so we wrap that in a promise here.
  if (!cb) {
    return new Promise((resolve, reject) => {
        this.run((err, data) => err ? reject(err) : resolve(data))
    })
  }

  // Here the rest of the function that natively works with a callback
}
  • Related