Anton
Anton

Reputation: 33

Discriminating between callback union types with Typescript

I'm converting some code to TS and want to implement this interface

let handler = new Handler()
  .on("data", (content) => /*something*/)
  .on("finished", (content) => /*something else*/);

but I've run into problems discriminating between callback types. Putting the callbacks into discriminating unions doesn't work because they have different signatures. This is what I've got so far:

interface Content {
    x: string,
    y: string,
}

type EventType = "data" | "error" | "finished";

type DataCallback = (content: string) => void;
type ErrorCallback = (content: string) => void;
type FinishedCallback = (content: Content) => void;

type EventCallback = DataCallback | ErrorCallback | FinishedCallback;

class Handler {
    ondata: DataCallback;
    onerror: ErrorCallback;
    onfinished: FinishedCallback;

    on(event: EventType, callback: EventCallback) {
        if (event === "data") this.ondata = callback;
        else if (event === "error") this.onerror = callback;
        else if (event === "finished") this.onfinished = callback;
        else console.error(`Unhandled event: ${event}`);

        return this;
    }
}

But this gives the following error(s):

      TS2322: Type 'EventCallback' is not assignable to type 'DataCallback'.
  Type 'FinishedCallback' is not assignable to type 'DataCallback'.
    Types of parameters 'content' and 'content' are incompatible.
      Type 'string' is not assignable to type 'Content'.

which totally makes sense.

Is this interface impossible to implement in Typescript?

One option is to change the interface to

onData(callback: DataCallback) { this.ondata = callback; }

etc, but I was hoping that I could keep the same interface.

Upvotes: 3

Views: 485

Answers (1)

Emma
Emma

Reputation: 855

You can use the same pattern as window.addEventListener.

You first make a map from your event types to the callback types they expect:

interface EventCallbacks {
    "data": DataCallback,
    "error": ErrorCallback,
    "content": ContentCallback,
}

And then you type your .on method as

on<E extends keyof EventMap>(event: E, callback: EventCallbacks[E]): Handler;

Now when writing on("data", ...) typescript will know that the second argument must be a DataCallback

Upvotes: 2

Related Questions