TypeScript
TypeScript copied to clipboard
Regression causes Function top-type to be callable with no arguments
Bug Report
🔎 Search Terms
function top type callable spread never regression
🕗 Version & Regression Information
- This changed between versions 3.8.3 and 3.9.7
⏯ Playground Link
Playground link with relevant code
💻 Code
declare let foo: (...args: never) => void;
foo = (x: string) => {};
foo(); // no error
🙁 Actual behavior
foo()
was permitted.
🙂 Expected behavior
foo()
should not be permitted because it is unsafe. (...args: never) => unknown
behaves as the top-type for all functions; all function types are assignable to it. So calling it will lead to unsafe behavior by functions expecting 1 or more arguments.
:wave: Hi, I'm the Repro bot. I can help narrow down and track compiler bugs across releases! This comment reflects the current state of the repro in the issue body running against the nightly TypeScript.
Issue body code block by @webstrand
:+1: Compiled
Historical Information
Version | Reproduction Outputs |
---|---|
4.2.2, 4.3.2, 4.4.2, 4.5.2, 4.6.2 |
:+1: Compiled |
@typescript-bot bisect good v3.8.3 bad v3.9.7
I think this aliasing unsoundness is actually unrelated to whether (...args: never) => void
is callable. Consider (...args: never[]) => void
or even (...args: string[]) => void
. The assignment to foo
in your example would still work, and yet these types are obviously callable with no arguments.
The change between v3.8.3 and v3.9.7 occurred at 0e15b9f2457b66ee9c14e42292ea5e5d9d78258e.
I've always considered this to be the distinction between (...args: never) => unknown
and (...args: never[]) => unknown
. The latter has to be callable with no arguments because never[]
is assignable from []
. Otherwise, without (...args: never) => unknown
, there is no top-type for all functions.
declare let foo: (...args: never[]) => void;
foo = (x: string) => {};
makes sense, fundementally, because [string]
is assignable to never[]
Otherwise, without
(...args: never) => unknown
, there is no top-type for all functions.
Well, (...args: never[]) => unknown
is still a function top type in that every other function type is assignable to it (the two formulations in question are cross-assignable). But it does feel like you ought to be able to express a function type constraint that is itself not callable because you have no idea what to pass it 🤔
For real-world use, this most commonly shows up in my code like
function foo<T extends (...args: never) => unknown>(fn: T) {
fn(); // no error
}
or
const registry = new Map<(...args: never) => unknown, unknown[]>();
for(const [fn, data] of registry) {
fn(); // no error
}
Well,
(...args: never[]) => unknown
is still a function top type in that every other function type is assignable to it
Actually I found a counterexample from AnyhowStep by reading around https://github.com/microsoft/TypeScript/issues/48840#issuecomment-1109002426: https://www.typescriptlang.org/play?#code/PQKhCgAIUhlBDAZgUwE6QG5oM4EsD2AdpPopAAYBiAroQMYAuBh5kA7gBa50eTYf5qAGwAmkQvgaQARskh14QochEA6KDACiWVAE9IiWo2Z8BwsbMjxseAOaF405ZAb4XXbABoZ1KQw+mgqLikjJy1nYOTnKukMg6+ob0TETq0MDgyAAeAA74qH66OXIAgoS6NMkmALyQABSqjfCottgAXOLxaADaALoAlJDVAHyY+LgiANzg4EnGRAb4+AA8ACpxWQzIhCLYVuV9w3WDAN5QkBd0RNhSiIQAjB0NjajINx2rgyNjE0P1jaoAPpfUYnAC+0wul2ut0IACYOmUKkYUsRand7pBgMA4qhUPlwGCgA
declare let foo: (...args: never[]) => void; foo = (x: string) => {};
makes sense, fundementally, because
[string]
is assignable tonever[]
But [string]
isn't assignable to never[]
. Seems that that there is another case of unsoundness with (...args: never[]) => unknown
.
let x: (...args: never[]) => unknown = () => {}
const y: (...args: ["one arg"]) => unknown = (x) => alert(x.charCodeAt(0))
x = y;
try {
x() // throws
} catch(e) {
alert(e)
}
I think it is just broken that (...args: never) => unknown
is callable.
So can we confirm that this is a bug and not just "needs investigation" anymore, given the discussion in #35438 ?
It seems there are two different unsoundnesses being discussed here:
declare let foo: (...args: never) => void;
foo(); // should error (`[]` isn't assignable to `never`), but does not
declare let foo: (...args: never[]) => void;
foo = (x: string) => {}; // should error (`never[]` isn't assignable to `[string]`), but does not
Both of these unsoundnesses relate to a top function type being callable; however, the correct fix for the former is to make the type (...args: never) => void
uncallable but still a top function type, while the correct fix for the latter is to make the type (...args: never[]) => void
not a top function type but still callable.
We shouldn't even let you write declare let foo: (...args: never) => void;
. It doesn't make sense.
The second assignment isn't unsound per our definition of rest args being assumed to have sufficient arity (or more accurately, it's unsound in the way that all rest args / finite parameter lists are unsound)
We shouldn't even let you write
declare let foo: (...args: never) => void;
. It doesn't make sense.
Are you saying that the type (...args: never) => void
doesn't make sense and should be removed? It seems perfectly logical as a top function type; the type of ...args
is, in a sense, the intersection of all possible argument types, which is never
. You can't call a function of type (...args: never) => void
, because you don't know what arguments it takes, but you know that it is something that could be theoretically called, you just don't know what it could be called with. It's like {}
being the top type of property-bearing things; you can't directly access properties on it, because you don't know what properties it might have, but you know that it has properties that could theoretically be accessed.
The second assignment isn't unsound per our definition of rest args being assumed to have sufficient arity (or more accurately, it's unsound in the way that all rest args / finite parameter lists are unsound)
Ah, good point.
Upon further reflection, I think you're right about (args: never)
So, just running into this again, presumably declare const f: (...args: never[]) => unknown
should allow f()
because []
is assignable to never[]
, but declare const g: (...args: never) => unknown
should not allow g()
because []
is not assignable to never
. And therefore (...args: never) => unknown
is a top type for functions but (...args: never[]) => unknown
shouldn't be.
Right now both types are callable with no arguments and both of these are considered top types for functions. Which of these can we fix without breaking everyone?
I've been using and teaching (...args: never) => unknown
as the supertype of all callables.
Would it break the universe if we stopped (...args: never[]) => unknown
from being considered a top type? No function that requires an argument should be assignable to it. (e.g., const f: (...args: never[])=>unknown = (x: string) => x.toUpperCase(); f();
compiles and explodes at runtime.) I'm guessing it's been used as a top type in various real-worldy places and these would be broken if the change were made. But I'd at least like to see an official "we know this is unsound but it's too late to touch it now" comment somewhere I can refer people to.