1

Why does TypeScript tell me "This overload signature is not compatible with its implementation signature"?

import { Client, ClientEvents } from 'discord.js';

class MyEvents {
  execute: (...args: ClientEvents[keyof ClientEvents]) => void;

  // This overload signature is not compatible with its implementation signature.
  constructor(execute: (client: Client<true>) => void);
  constructor(execute: (...args: ClientEvents[keyof ClientEvents]) => void) {
    this.execute = execute;
  }
}

While the code below works perfectly.

type MinimalFn = (client: Client<true>) => void;
let myFunc: MinimalFn = function (...args: ClientEvents[keyof ClientEvents]) {};

3 Answers 3

1

The Issue is -

  1. The first constructor overload (client: Client<true>) => void implies a function with one specific argument of type Client<true>.

  2. The second (actual implementation) accepts (...args: ClientEvents[keyof ClientEvents]), which is a variadic tuple depending on the event.

These two are not compatible — TypeScript expects all overloads to be assignable to the implementation signature.

Just do one thing, If you always pass (...args: ClientEvents[keyof ClientEvents]), just remove the first overload:

class MyEvents {
  execute: (...args: ClientEvents[keyof ClientEvents]) => void;

  constructor(execute: (...args: ClientEvents[keyof ClientEvents]) => void) {
    this.execute = execute;
  }
}
Sign up to request clarification or add additional context in comments.

4 Comments

When you say "TypeScript expects all overloads to be assignable to the implementation signature." isn't it the opposite?
And I don't understand why my function overload can't match with my implementation if the code below work fine. type MinimalFn = (client: Client<true>) => void; let myFunc: MinimalFn = function (...args: ClientEvents[keyof ClientEvents]) {};
Variadic-ness itself isn't the issue. The first argument slot of the callback is not compatible, as you can see in this example without a rest parameter. The problem is that ClientEvents[keyof ClientEvents][0] is not assignable to Client<true>.
@xGeekoo you're being tripped up by variance. The example in your comment doesn't involve a callback, so the subtyping relationship is reversed. These additional examples may help.
0

Overloads in TypeScript need to have a more broad implementation definition, with the overloads being more specific

For example:

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
    return /* ... */;
}

/*
Usually, the last would be written like
function add(a: number | string, b: number | string): number | string {
    return /* ... *​/;
}
*/
const foo = add(5, 3);
//    ^?  number
const foo2 = add("3", "6");
//    ^?  string
const foo3 = add(3, "2");
//    ^?  error "No overload matches this call"
/*
The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.
*/

Comments

0

To explain why your first code block has a type error while your second does not, let's first consider this simple example:

const success: string | number = ''

const failure: (a: string | number) => void = (b: string) => console.log(b.toUpperCase())
//    ^^^^^^^
// Type '(b: string) => void' is not assignable to type '(a: string | number) => void'.
//   Types of parameters 'b' and 'a' are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

I'm allowed to assign a string value to a variable of type string | number, but I'm not allowed to assign a function which only accepts a string to a function type which accepts string | number. The type of failure says that failure(42) should be a legal call, but this would blow up at runtime (numbers don't have a .toUpperCase() method). The generalized way to describe this is that function types are contravariant in their parameters.

In your case the parameter is itself a function type, so there is one more variance flip to consider:

const success: (a: string) => void = (b: string | number) => {}

const failure: (a: (a1: string) => void) => void = (b: (b1: string | number) => void) => b(42)
//    ^^^^^^^
// Type '(b: (b1: string | number) => void) => void' is not assignable to type '(a: (a1: string) => void) => void'.
//   Types of parameters 'b' and 'a' are incompatible.
//     Types of parameters 'a1' and 'b1' are incompatible.
//       Type 'string | number' is not assignable to type 'string'.
//         Type 'number' is not assignable to type 'string'.

In this case the type of failure says I should be allowed to call failure((a1: string) => console.log(a1.toUpperCase())), but this would also end up blowing up because we'd invoke .toUpperCase() on a number. TypeScript is trying to save us from such runtime errors.

In your case the relevant types are ClientEvents[keyof ClientEvents][0] and Client<true> rather than string | number and string. ClientEvents[keyof ClientEvents][0] is not assignable to Client<true>.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.