Giter VIP home page Giter VIP logo

Comments (14)

fatcerberus avatar fatcerberus commented on July 17, 2024 1

@MartinJohns By distribution over unions I think he means that given the overloads...

declare function foo(v: 1): void;
declare function foo(v: 2): void;

...it should be legal to call foo(v) with a v typed as 1 | 2. Which comes up semi-regularly and I'm almost positive there's at least one open issue about it. My understanding was that the reason that isn't done is for performance reasons because it becomes combinatorially explosive in the number of parameters and union members.

from typescript.

rotu avatar rotu commented on July 17, 2024 1

it's one case per overload signature

It's not though - if all the overloads aren't there, it might be able to short-circuit once it finds a failing case, but in principle it still has to enumerate all the possible combinations of union members. The "40 questions" bit I described would still happen, even if the matching overloads aren't there.

Okay I think I understand. You're assuming the implementation will be "normalize the types of both the arguments and the parameters to unions of tuple types and check that every argument case is covered by a parameter case". This is a really natural approach to take - canonicalizing to a sum (union) of products (Cartesian product).

If so, then yes, it is an expensive check! But that assumes the worst-case scenario. You don't need to eagerly expand out the argument nor the parameter types into unions. Keep the argument type as a tuple and only split into cases when an overload partially matches the argument type.

e.g. for arguments [A, B] and an overload with parameters [D, E], the case [A&D, B&E] is handled by the overload. You then need to check [Exclude<A,D>, B] | [A, Exclude<B,E>] against the other overloads. If A is disjoint from D, it simplifies to [A,B]. And if A is a subset of D, it simplifies to [A, Exclude<B,E>].

You could also try to gather the argument type into a product of sums (e.g. foo) but I expect that's not very useful in practice - if the function can be declared that way, it probably is.

TypeScript already allows assigning a tuple of union types to a union of tuple types and vice versa.

Indeed, but IIRC there's already a (fairly low) limit to the size of the unions that can be expanded out during checking that way before it just fails. Something like 25 if my memory is right.

Seems like you're right! And it doesn't even give a meaningful error message!
This does strongly suggest that a casewise analysis like the above is being used in the code. And that it's an opportunity for optimization.

EDIT: the code - your memory is good!

Playground Link

from typescript.

MartinJohns avatar MartinJohns commented on July 17, 2024

You forgot to fill out the issue template.


type X = ReturnType<Not> // boolean; false if signature 3 is omitted

This is working as intended and documented: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types

When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from the last signature (which, presumably, is the most permissive catch-all case).


It is unclear to this author whether the implementation signature belongs in a pure declaration.

Implementation signatures can't be part of interfaces because interfaces don't have implementations. Whether you should have a permissive "catch all" signature as part of your interface or not really depends on your use case.

The only weird thing here is that true as any resolves to the true overload, but I feel I've seen this in another issue already.

from typescript.

rotu avatar rotu commented on July 17, 2024

Implementation signatures can't be part of interfaces because interfaces don't have implementations. Whether you should have a permissive "catch all" signature as part of your interface or not really depends on your use case.

Yes, I'm confusing the terms "implementation signature" and "catch all signature". It seems like catch-all signatures are a workaround inspired by implementation signatures. And they would largely not be needed if function overload distributed over union.

The only weird thing here is that true as any resolves to the true overload, but I feel I've seen this in another issue already.

That's my understanding too. Maybe a duplicate of #39833?

from typescript.

MartinJohns avatar MartinJohns commented on July 17, 2024

Yes, I'm confusing the terms "implementation signature" and "catch all signature".

function foo(v: 1): void;
function foo(v: 2): void;
function foo(v: 1 | 2): void; // catch all overload, just another overload signature that's visible to the caller
function foo(v: 1 | 2): void { // implementation signature, not visible for caller
    // implementation be here
}

And they would largely not be needed if function overload distributed over union.

No idea what you mean. This behavior is intentional.

That's my understanding too. Maybe a duplicate of #39833?

Yes, that's the one.

from typescript.

rotu avatar rotu commented on July 17, 2024

@MartinJohns By distribution over unions I think he means that given the overloads...

declare function foo(v: 1): void;
declare function foo(v: 2): void;

...it should be legal to call foo(v) with a v typed as 1 | 2. Which comes up semi-regularly and I'm almost positive there's at least one open issue about it. My understanding was that the reason that isn't done is for performance reasons because it becomes combinatorially explosive in the number of parameters and union members.

Exactly. In this your example, it simplifies out to void. In general, the return types of chained calls will tend to unknown or any. Only if the output types are disjoint will you get the combinatorial growth (and it’s a sum not the product behavior of generic or template string types!).

from typescript.

fatcerberus avatar fatcerberus commented on July 17, 2024

The combinatorial behavior I’m talking isn’t about the return types, but how much work has to be done to typecheck the call sites. Basically if there are multiple arguments, each with a union type, you’d have to take each union apart and, for each member, check if it matches one of the overloads. That’s on average exponential in the number of arguments provided.

from typescript.

rotu avatar rotu commented on July 17, 2024

The combinatorial behavior I’m talking isn’t about the return types, but how much work has to be done to typecheck the call sites. Basically if there are multiple arguments, each with a union type, you’d have to take each union apart and, for each member, check if it matches one of the overloads. That’s on average exponential in the number of arguments provided.

I’m still not seeing it. Could you give a concrete example?

from typescript.

fatcerberus avatar fatcerberus commented on July 17, 2024

Suppose you have

declare function foo(x: A, y: A): void;
declare function foo(x: A, y: B): void;
declare function foo(x: B, y: A): void;
declare function foo(x: B, y: B): void;

declare let x: A | B;
declare let y: A | B;

foo(x, y);

This call should succeed according to the types. But to know that, you have to answer all of these in the affirmative:

  • Is foo(A, A) a valid call?
  • Is foo(A, B) a valid call?
  • Is foo(B, A) a valid call?
  • Is foo(B, B) a valid call?

and each of those individual checks has to check all the overloads for a match. Now increase the size of the unions and/or the number of parameters and this very quickly gets out of hand.

from typescript.

rotu avatar rotu commented on July 17, 2024
  1. That's not a combinatorial explosion in checking difficulty - it's one case per overload signature. Yes, it would require you to declare combinatorially many cases, but if the function can be factored into a single signature, it makes sense to do so (foo(x:A|B, y:A|B): void). If not, then the most general signature is awkward anyway.
  2. TypeScript already allows assigning a tuple of union types to a union of tuple types and vice versa. That's morally the same process:
    declare let x: [A,A] | [A,B] | [B,A] | [B,B]
    declare let y: [A|B, A|B]
    x = y

from typescript.

fatcerberus avatar fatcerberus commented on July 17, 2024

it's one case per overload signature

It's not though - if all the overloads aren't there, it might be able to short-circuit once it finds a failing case, but in principle it still has to enumerate all the possible combinations of union members. The "40 questions" bit I described would still happen, even if the matching overloads aren't there.

TypeScript already allows assigning a tuple of union types to a union of tuple types and vice versa.

Indeed, but IIRC there's already a (fairly low) limit to the size of the unions that can be expanded out during checking that way before it just fails. Something like 25 if my memory is right.

from typescript.

RyanCavanaugh avatar RyanCavanaugh commented on July 17, 2024

Overload resolution selects the first matching signature (everyone loves it when language use simple algorithms, right? right??). There have been requests to do something more involved when any is involved, but none of them have really succeeded in staking out a definition doesn't break huge swathes of user code; many times overloads are arranged with a "common" case first and an "uncommon" case second and people want the "common" one to be selected in an any invocation.

The one caveat to "first" is that there's an initial pass that tries to discard assignments to any, so if you really need your interface to do a particular thing on any, you can declare that without breaking more-specific inferences:

interface Not{
    (x: any): boolean;

    (x:false):true // signature 1
    (x:true):false // signature 2

    (x:boolean):boolean // signature 3. Unclear whether this should be declared or not
}
declare var not:Not
const p = not(true);
//    ^? still 'false'

from typescript.

rotu avatar rotu commented on July 17, 2024

The one caveat to "first" is that there's an initial pass that tries to discard assignments to any, so if you really need your interface to do a particular thing on any, you can declare that without breaking more-specific inferences

Clever workaround, but that defeats the type annotation on the argument.
Somehow the only workaround that seems to work is to make it a generic function:

interface Not2 {
   <const T extends boolean> (x:T) : T extends true ? false : T extends false ? true : never
}

Overload resolution selects the first matching signature.

Somehow though, adding (or moving) the catch-all signature first results in matching the second overload, with weird results:

interface Not{
    (x: boolean): boolean;
    (x: false): true;
    (x: true): false;
    (x: boolean): boolean;
}

declare var not:Not
const notAny = not(true as any) // true !?!?
//    ^?

Playground Link

It seems in intellisense that the boolean-accepting overloads are being shuffled to number 3 and 4 in the list!

Screenshot 2024-07-09 154002

from typescript.

typescript-bot avatar typescript-bot commented on July 17, 2024

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

from typescript.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.