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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.