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

TypeScript should infer relationship between function argument types from overloads #57153

Closed
6 tasks done
paskozdilar opened this issue Jan 24, 2024 · 5 comments
Closed
6 tasks done
Labels
Duplicate An existing issue was already created

Comments

@paskozdilar
Copy link

paskozdilar commented Jan 24, 2024

πŸ” Search Terms

typescript function argument conditional type overloads automatic infer

βœ… Viability Checklist

⭐ Suggestion

It would be nice if compiler could use the overloaded function declarations to automatically infer information about types in the implementation, to avoid unnecessary type checking.

πŸ“ƒ Motivating Example

https://tsplay.dev/w1LKOW

// The compiler forces us to type check the `value` in the implementation...
function Foo(value: number, ok: true): void;
function Foo(value: undefined, ok: false): void;
function Foo(...args: [number, true] | [undefined, false]): void;
function Foo(value: number | undefined, ok: boolean): void {
  var n: number;
  var u: undefined;
  if (ok) {
    n = value; // ERROR: Type 'number | undefined' is not assignable to type 'number'.
  } else {
    u = value; // ERROR: Type 'number | undefined' is not assignable to type 'undefined'.
  }
}

// ...even though the function overloads guarantee that the above cannot happen:
Foo(42, false); // ERROR: No overload matches this call.
Foo(undefined, true); // ERROR: No overload matches this call.

declare var value: number | undefined;
declare var ok: boolean;
Foo(value, ok); // ERROR: No overload matches this call.

declare var args: [number, true] | [undefined, false];
Foo(...args); // No error: arguments are fine

πŸ’» Use Cases

  1. What do you want to use this for?

I want to define a function which can receive a union tuple and have an efficient implementation of it.

  1. What shortcomings exist with current approaches?

Unnecessary runtime checking required.

  1. What workarounds are you using in the meantime?

Runtime checking.

@MartinJohns
Copy link
Contributor

You can do:

function Foo(value: number, ok: true): void;
function Foo(value: undefined, ok: false): void;
function Foo(...[value, ok]: [number, true] | [undefined, false]): void { ... }

@paskozdilar
Copy link
Author

That's another workaround, sure, but I have trouble understanding why TypeScript can't infer the case above.
Why does one construct work, but another (equivalent one) doesn't?
Is there some kind of rule I can follow to know in advance when something isn't working, and I should use alternative, equivalent format to achieve the thing?

Sometimes it feels like trying to make TypeScript work with me instead of against me is a game of cat and mouse.

@craigphicks
Copy link

craigphicks commented Jan 24, 2024

That's another workaround, sure, but I have trouble understanding why TypeScript can't infer the case above. Why does one construct work, but another (equivalent one) doesn't? Is there some kind of rule I can follow to know in advance when something isn't working, and I should use alternative, equivalent format to achieve the thing?

It could be (but isn't currently) possible to write

function Foo(value: number, ok: true): void;
function Foo(value: undefined, ok: false): void;
function Foo(value,ok) { ... }

because the necessary info is already in the overloads, as you say. In fact I recently read an old ts issue saying exactly that - I can't find it right it now though. So that part of your issue is a duplicate. Likewise for type checking the implementation.

Here is one way your code could be rewritten to avoid compile errors, yet handle all possibilities safely:

// The compiler forces us to type check the `value` in the implementation...
// function Foo(value: number, ok: true): void;
// function Foo(value: undefined, ok: false): void;
function Foo(...[value,ok]: [number,true]|[undefined,false]): void;
function Foo(...[value,ok]: [number,true]|[undefined,false]|[number|undefined,boolean]): void; // | throws "rangeError"
//function Foo(value: number | undefined, ok: boolean): void; // throws "rangeError"
//function Foo(value: number | undefined, ok: boolean): void {
function Foo(...[value,ok]: [number,true]|[undefined,false]|[number|undefined,boolean]): void {
    if (ok && typeof value === "number"){
        value; // number
    }
    else if (!ok && typeof value === "undefined"){
        value; // number
    }
    throw "rangeError";
}

// ... you wrote these apparently meaning that Foo might be called with rangeError inputs. 
try {
  Foo(42, false); // no ERROR: 
  Foo(undefined, true); // no ERROR: 
}
catch(e){}

// ... you wrote these apparently meaning that Foo might be called with rangeError inputs. 
declare var value: number | undefined;
declare var ok: boolean;

try {
  Foo(value, ok); // no ERROR
} catch (e) {}

// ... you wrote this safe in the knowledge that there is no rangeError inputs so try/catch not needed
declare var args: [number, true] | [undefined, false];
Foo(...args); // no ERROR

There is an ambiguity in your code comment "...even though the function overloads guarantee that the above cannot happen:".
Overloads cannot guarantee that a user will not pass rangeError data. They can only guarantee that a compiler error will be raised. But a compiler error can be a nuisance too - as in your call to Foo(value, ok);.

See #57057 for a proposal to less the verbiage in the final overload. (Additional proposal 2).

You will notice that the overloads have been rewritten entirely in ...[] format, and the others commented out. That because your last statement Foo(...args); gives an error otherwise. That is a bug. #57156

@jcalz
Copy link
Contributor

jcalz commented Jan 25, 2024

Duplicate of or strongly related to #13235, #44262, #52478 .

#52478 (comment)

"Re-check the body using each overload signature" turns this into a huge problem [...] we'd need to make big advances in control flow, somewhat fundamental changes to how the language service thinks about the type of a node, and possibly invent new sorts of types altogether.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Jan 26, 2024
@typescript-bot
Copy link
Collaborator

This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jan 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

6 participants