Function overloading of generic functions allows for invalid code
🔎 Search Terms
function overloading generic functions invalid code
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about "function overloading generic"
⏯ Playground Link
https://tsplay.dev/N5Y35N
💻 Code
/* overload #1 */ function f0(fn: () => void): (() => void);
/* overload #2 */ function f0<P1 extends {}>(fn: (p1: P1) => void): ((p1: P1) => void);
/* overload #3 */ function f0<P1 extends {}, P2 extends {}>(fn: (p1: P1, p2: P2) => void): ((p1: P1, p2: P2) => void);
function f0(fn: (...args: unknown[]) => void): (...args: unknown[]) => void {
fn(); // Creates a bug in overloads #2 and #3, given both p1 and p2 will be undefined, even though P1 and P2 do not allow it.
fn(1); // Should only be valid if P1 extends number; in addition p2 will be undefined, even though P2 does not allow it.
fn(1, 2); // Should only be valid if P1 and P2 both extend number.
return () => {};
}
f0((a: string) => { console.log('a: ', a, 'typeof a: ', typeof a)}) // Prints: "a: ", undefined, "typeof a: ", "undefined"
🙁 Actual behavior
TS compiles the code above as if it were correct, even though the following code correctly triggers an error for i and j:
const h: ((...args: unknown[]) => void) = (() => {})
const i: ((...args: unknown[]) => void) = ((p1: string) => {})
const j: ((...args: unknown[]) => void) = ((p1: string, p2: boolean) => {})
🙂 Expected behavior
TS should warn about the potencial error.
Notice how TS detects an error in a similar situation, when the functions are NOT generic:
/* overload #1 */ function f0(fn: () => void): (() => void);
/* overload #2 */ function f0(fn: (p1: string) => void): ((p1: string) => void);
/* overload #3 */ function f0(fn: (p1: string, p2: boolean) => void): ((p1: string, p2: boolean) => void);
But fail to detect it in the presence of generic functions.
Additional information about the issue
In the following similar code, but using a single tuple parameter, TS does detect the potencial error, triggering the "This overload signature is not compatible with its implementation signature" message:
function f1(fn: (a: []) => boolean): number;
function f1<P1>(fn: (a: [P1]) => boolean): string;
function f1<P1, P2>(fn: (a: [P1, P2]) => boolean): boolean;
function f1(fn: (a: unknown[]) => boolean): any {
fn([1, 2, 3]);
}
It makes sense, since these expressions are all invalid:
const f: ((a: unknown[]) => boolean) = ((a: []) => true)
const g: ((a: unknown[]) => boolean) = ((a: [number]) => true)
const h: ((a: unknown[]) => boolean) = ((a: [number, string]) => true)
Overloads are checked more loosely than they "should" be, see #13235 among others. You'll probably always be able to find some inconsistency in exactly what is allowed versus rejected.
Joe and Andrew, I agree that it's virtually impossible for TS to cover all use cases. However, as TS already detects the error in the case below (Playground):
/* overload #1 */ function f0(fn: () => void): (() => void);
/* overload #2 */ function f0(fn: (p1: string) => void): ((p1: string) => void);
/* overload #3 */ function f0(fn: (p1: string, p2: boolean) => void): ((p1: string, p2: boolean) => void);
function f0(fn: (...args: unknown[]) => void): (...args: unknown[]) => void {
// ...
}
I don't see why it fails to detect the same situation when generic types are present, like the similar code below, considering that string is (sort of) a "subset" (i.e. contained in) of the generic type P1 and boolean is a "subset" of P2 (Playground):
/* overload #1 */ function f0(fn: () => void): (() => void);
/* overload #2 */ function f0<P1>(fn: (p1: P1) => void): ((p1: P1) => void);
/* overload #3 */ function f0<P1, P2>(fn: (p1: P1, p2: P2) => void): ((p1: P1, p2: P2) => void);
This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.