Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Regression causes Function top-type to be callable with no arguments #48840

Closed
webstrand opened this issue Apr 25, 2022 · 18 comments Β· Fixed by #52387
Closed

Regression causes Function top-type to be callable with no arguments #48840

webstrand opened this issue Apr 25, 2022 · 18 comments Β· Fixed by #52387
Labels
Bug A bug in TypeScript Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros
Milestone

Comments

@webstrand
Copy link
Contributor

webstrand commented Apr 25, 2022

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.

@andrewbranch andrewbranch added Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros Bug A bug in TypeScript labels Apr 25, 2022
@typescript-bot
Copy link
Collaborator

typescript-bot commented Apr 25, 2022

πŸ‘‹ 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

πŸ‘ Compiled

Historical Information
Version Reproduction Outputs
4.2.2, 4.3.2, 4.4.2, 4.5.2, 4.6.2

πŸ‘ Compiled

@andrewbranch
Copy link
Member

@typescript-bot bisect good v3.8.3 bad v3.9.7

@andrewbranch
Copy link
Member

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.

@andrewbranch andrewbranch added Needs Investigation This issue needs a team member to investigate its status. and removed Bug A bug in TypeScript labels Apr 25, 2022
@typescript-bot
Copy link
Collaborator

The change between v3.8.3 and v3.9.7 occurred at 0e15b9f.

@webstrand
Copy link
Contributor Author

webstrand commented Apr 25, 2022

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.

@webstrand
Copy link
Contributor Author

declare let foo: (...args: never[]) => void;
foo = (x: string) => {};

makes sense, fundementally, because [string] is assignable to never[]

@andrewbranch
Copy link
Member

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 πŸ€”

@webstrand
Copy link
Contributor Author

webstrand commented Apr 25, 2022

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
}

@jack-williams
Copy link
Collaborator

jack-williams commented Apr 26, 2022

declare let foo: (...args: never[]) => void;
foo = (x: string) => {};

makes sense, fundementally, because [string] is assignable to never[]

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.

@jcalz
Copy link
Contributor

jcalz commented Jun 1, 2022

So can we confirm that this is a bug and not just "needs investigation" anymore, given the discussion in #35438 ?

@andrewbranch andrewbranch added Bug A bug in TypeScript and removed Needs Investigation This issue needs a team member to investigate its status. labels Jun 1, 2022
@tjjfvi
Copy link
Contributor

tjjfvi commented Oct 6, 2022

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.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Oct 6, 2022

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)

@tjjfvi
Copy link
Contributor

tjjfvi commented Oct 6, 2022

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.

@RyanCavanaugh
Copy link
Member

Upon further reflection, I think you're right about (args: never)

@jcalz
Copy link
Contributor

jcalz commented Dec 30, 2022

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?

@webstrand
Copy link
Contributor Author

I've been using and teaching (...args: never) => unknown as the supertype of all callables.

@jcalz
Copy link
Contributor

jcalz commented Mar 6, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants