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

Merge overloads that only differ by return type when intersecting #30520

Closed
wants to merge 5 commits into from

Conversation

kring
Copy link

@kring kring commented Mar 21, 2019

Consider two interfaces:

interface One {
  foo(id: string): { one: number };
}

interface Two {
  foo(id: string): { two: number };
}

Both interfaces define a method named foo taking identical parameters. However, the return types are different. If we write a class that implements both of these interfaces, TypeScript understands that we can satisfy both interfaces by returning an object with both properties:

class Both implements One, Two {
  foo(id: string): { one: number, two: number } {
    return {
      one: 1,
      two: 2
    };
  }
}

const both = new Both();
const result: { one: number, two: number } = both.foo('test');

In fact, if my imagination isn't failing me, this return type (or something that is assignable to it) is the only way we can satisfy both interfaces.

However, if we use intersection (&) instead of implements:

type BothIntersection = One & Two;
const bothIntersection: BothIntersection = both;
const resultIntersection: { one: number, two: number } = bothIntersection.foo('test');

The last line fails to type check. TypeScript reports:

Property 'two' is missing in type '{ one: number; }' but required in type '{ one: number; two: number; }'.ts(2741)

This is because the intersection has turned the two versions of foo into overloads instead of intersecting their return types.

In my possibly naive opinion, it makes no sense for & to create overloads where the parameter types are identical, since we can't select an overload based solely on the return type. And the inconsistency with interface implementation seems undesirable as well. TypeScript should continue to create overloads where the parameters are different, but, for functions with identical parameters, the signatures should be collapsed to a single signature where the return type is the intersection of the return types of all the signatures with the same parameters.

So that is what this pull request implements.

All previous tests passed except for tests\cases\conformance\jsx\checkJsxGenericTagHasCorrectInferences.tsx, which now reports slightly different compiler errors. I believe that the errors are actually better in this branch (see the baseline diff).

I'm certain I haven't gotten everything right here, as I only looked at the TypeScript code for the first time today. Hats off to you all for making it so easy to understand! But if you agree this PR is heading in the right direction I'm very happy to improve it as necessary.

@msftclas
Copy link

msftclas commented Mar 21, 2019

CLA assistant check
All CLA requirements met.

@kring
Copy link
Author

kring commented Mar 22, 2019

A few notes:

  • Here's an old issue reporting the problem that this PR fixes: Intersected function type returns only first constituent's return type #10508. The last comment proposes what I have implemented here.
  • Section 3.11.1 of the spec says "When one or more constituent types of I have a call signature S, I has the apparent call signature S. The signatures are ordered as a concatenation of the signatures of each constituent type in the order of the constituent types within I." This PR changes that behavior, and I realize that makes this PR harder to accept. However, almost the exact same is said of construct signatures in the next bullet of the spec, and yet Mixin classes #13743 changed the intersection behavior in a very similar way for some construct signatures in order to enable mixins. I think this PR can be seen as a generalization of the intersection behavior in Mixin classes #13743.
  • I don't believe this PR will cause any code that previously compiled to now fail to compile, as it will only add apparent members to the return type.
  • Currently in this PR the function parameter types must be identical in order to intersect the return types, but it may be desirable to merge related parameters types to the narrower type. Certainly that would have wider-ranging impacts on the language, though. The current implementation, though, as far as I can see, will have quite a limited and safe impact (recognizing that I'm not entirely qualified to assess that).
  • I've realized today that it is probably necessary to verify that the functions have identical type parameter types, in addition to identical function parameter types. I will implement that soon.

Thanks for taking a look! As an open source maintainer myself, I realize this sort of PR is really difficult to accept from the community. With any luck, I'm not too far off the mark. If nothing else, perhaps this will serve as some inspiration and maybe even a starting point if the core developers decide to tackle it in the future.

@RyanCavanaugh
Copy link
Member

@typescript-bot test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Apr 25, 2019

Heya @RyanCavanaugh, I've started to run the extended test suite on this PR at 35f064d. You can monitor the build here. It should now contribute to this PR's status checks.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Apr 25, 2019

@typescript-bot test this because it was out of date with master

@RyanCavanaugh
Copy link
Member

@typescript-bot perf test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Apr 25, 2019

Heya @RyanCavanaugh, I've started to run the perf test suite on this PR at 8896207. You can monitor the build here. It should now contribute to this PR's status checks.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

@RyanCavanaugh
The results of the perf run you requested are in!

Here they are:

Comparison Report - master..30520

Metric master 30520 Delta Best Worst
Angular - node (v6.5.0, x64)
Memory used 340,646k (± 0.41%) 342,114k (± 0.39%) +1,468k (+ 0.43%) 339,378k 343,482k
Parse Time 1.63s (± 1.25%) 1.63s (± 0.68%) +0.01s (+ 0.31%) 1.61s 1.66s
Bind Time 1.56s (± 2.02%) 1.57s (± 1.41%) +0.01s (+ 0.58%) 1.52s 1.61s
Check Time 4.90s (± 0.69%) 4.82s (± 0.40%) -0.07s (- 1.49%) 4.78s 4.87s
Emit Time 6.38s (± 1.90%) 6.49s (± 1.63%) +0.12s (+ 1.84%) 6.26s 6.68s
Total Time 14.46s (± 0.84%) 14.52s (± 0.66%) +0.06s (+ 0.39%) 14.30s 14.72s
Monaco - node (v6.5.0, x64)
Memory used 364,365k (± 0.09%) 364,342k (± 0.09%) -23k (- 0.01%) 364,108k 365,662k
Parse Time 1.27s (± 0.63%) 1.27s (± 0.49%) -0.00s (- 0.24%) 1.25s 1.28s
Bind Time 1.52s (± 0.65%) 1.51s (± 0.43%) -0.01s (- 0.40%) 1.50s 1.53s
Check Time 5.76s (± 1.23%) 5.58s (± 0.59%) -0.18s (- 3.06%) 5.51s 5.66s
Emit Time 3.60s (± 3.23%) 3.52s (± 2.43%) -0.08s (- 2.17%) 3.22s 3.65s
Total Time 12.15s (± 1.26%) 11.89s (± 0.90%) -0.26s (- 2.16%) 11.53s 12.04s
TFS - node (v6.5.0, x64)
Memory used 321,071k (± 0.01%) 321,108k (± 0.01%) +36k (+ 0.01%) 321,062k 321,141k
Parse Time 0.99s (± 0.51%) 0.98s (± 0.48%) -0.00s (- 0.20%) 0.97s 0.99s
Bind Time 1.27s (± 0.76%) 1.27s (± 0.44%) +0.00s (+ 0.24%) 1.26s 1.28s
Check Time 4.41s (± 0.53%) 4.33s (± 0.68%) -0.08s (- 1.72%) 4.28s 4.42s
Emit Time 3.13s (± 0.49%) 3.16s (± 0.48%) +0.02s (+ 0.80%) 3.12s 3.18s
Total Time 9.79s (± 0.31%) 9.74s (± 0.33%) -0.05s (- 0.52%) 9.68s 9.84s
Angular - node (v6.5.0, x86)
Memory used 191,012k (± 0.00%) 191,060k (± 0.01%) +48k (+ 0.03%) 191,033k 191,090k
Parse Time 1.47s (± 0.48%) 1.46s (± 0.53%) -0.01s (- 0.54%) 1.45s 1.48s
Bind Time 1.51s (± 1.32%) 1.50s (± 0.98%) -0.01s (- 0.46%) 1.47s 1.54s
Check Time 4.81s (± 1.65%) 4.78s (± 2.12%) -0.03s (- 0.60%) 4.63s 5.01s
Emit Time 6.09s (± 1.77%) 6.11s (± 1.92%) +0.02s (+ 0.28%) 5.98s 6.38s
Total Time 13.88s (± 1.32%) 13.85s (± 1.63%) -0.03s (- 0.19%) 13.58s 14.38s
Monaco - node (v6.5.0, x86)
Memory used 204,349k (± 0.01%) 204,376k (± 0.01%) +28k (+ 0.01%) 204,335k 204,434k
Parse Time 1.26s (± 0.94%) 1.27s (± 0.66%) +0.00s (+ 0.24%) 1.25s 1.28s
Bind Time 1.55s (± 0.48%) 1.55s (± 0.38%) -0.01s (- 0.45%) 1.53s 1.56s
Check Time 4.70s (± 1.76%) 4.62s (± 1.77%) -0.08s (- 1.75%) 4.50s 4.79s
Emit Time 3.15s (± 2.64%) 3.13s (± 2.58%) -0.02s (- 0.67%) 2.96s 3.28s
Total Time 10.67s (± 0.34%) 10.56s (± 0.26%) -0.11s (- 0.98%) 10.49s 10.62s
TFS - node (v6.5.0, x86)
Memory used 180,333k (± 0.01%) 180,352k (± 0.01%) +19k (+ 0.01%) 180,335k 180,383k
Parse Time 0.98s (± 0.35%) 0.97s (± 0.49%) -0.00s (- 0.41%) 0.97s 0.99s
Bind Time 1.32s (± 0.73%) 1.31s (± 0.38%) -0.01s (- 0.99%) 1.30s 1.32s
Check Time 3.93s (± 0.55%) 3.84s (± 0.44%) -0.09s (- 2.19%) 3.81s 3.88s
Emit Time 2.54s (± 0.96%) 2.56s (± 0.69%) +0.02s (+ 0.59%) 2.52s 2.61s
Total Time 8.76s (± 0.36%) 8.68s (± 0.27%) -0.08s (- 0.95%) 8.64s 8.74s
Angular - node (v8.9.0, x64)
Memory used 330,723k (± 0.02%) 330,744k (± 0.01%) +21k (+ 0.01%) 330,615k 330,807k
Parse Time 1.81s (± 0.21%) 1.80s (± 0.52%) -0.01s (- 0.28%) 1.78s 1.82s
Bind Time 1.36s (± 1.01%) 1.35s (± 0.96%) -0.01s (- 0.44%) 1.33s 1.39s
Check Time 4.69s (± 1.44%) 4.60s (± 1.43%) -0.09s (- 1.96%) 4.47s 4.72s
Emit Time 5.92s (± 2.37%) 5.83s (± 3.86%) -0.09s (- 1.57%) 5.55s 6.36s
Total Time 13.78s (± 0.80%) 13.59s (± 1.33%) -0.20s (- 1.41%) 13.31s 14.01s
Monaco - node (v8.9.0, x64)
Memory used 358,471k (± 0.01%) 358,512k (± 0.02%) +41k (+ 0.01%) 358,412k 358,697k
Parse Time 1.45s (± 0.47%) 1.45s (± 0.40%) -0.01s (- 0.34%) 1.44s 1.46s
Bind Time 1.53s (± 1.00%) 1.53s (± 0.84%) +0.01s (+ 0.46%) 1.51s 1.56s
Check Time 4.85s (± 1.86%) 4.79s (± 1.45%) -0.06s (- 1.32%) 4.69s 4.95s
Emit Time 3.11s (± 5.61%) 3.15s (± 4.94%) +0.04s (+ 1.32%) 2.81s 3.31s
Total Time 10.94s (± 0.99%) 10.92s (± 0.94%) -0.02s (- 0.18%) 10.69s 11.05s
TFS - node (v8.9.0, x64)
Memory used 313,838k (± 0.01%) 313,874k (± 0.02%) +35k (+ 0.01%) 313,744k 313,968k
Parse Time 1.15s (± 0.39%) 1.15s (± 0.65%) +0.00s (+ 0.17%) 1.14s 1.17s
Bind Time 1.24s (± 1.40%) 1.24s (± 1.34%) -0.00s (- 0.24%) 1.21s 1.29s
Check Time 4.22s (± 0.90%) 4.24s (± 0.50%) +0.02s (+ 0.38%) 4.20s 4.30s
Emit Time 3.10s (± 1.66%) 3.14s (± 1.08%) +0.04s (+ 1.23%) 3.01s 3.18s
Total Time 9.71s (± 0.61%) 9.76s (± 0.41%) +0.05s (+ 0.54%) 9.66s 9.85s
Angular - node (v8.9.0, x86)
Memory used 187,171k (± 0.03%) 187,198k (± 0.01%) +28k (+ 0.01%) 187,142k 187,247k
Parse Time 1.76s (± 0.95%) 1.75s (± 0.66%) -0.00s (- 0.23%) 1.73s 1.78s
Bind Time 1.53s (± 0.71%) 1.53s (± 1.07%) -0.01s (- 0.39%) 1.50s 1.58s
Check Time 4.30s (± 0.77%) 4.25s (± 0.42%) -0.05s (- 1.26%) 4.20s 4.29s
Emit Time 5.63s (± 1.08%) 5.60s (± 1.05%) -0.04s (- 0.64%) 5.48s 5.71s
Total Time 13.22s (± 0.74%) 13.12s (± 0.57%) -0.10s (- 0.78%) 12.94s 13.29s
Monaco - node (v8.9.0, x86)
Memory used 199,814k (± 0.01%) 199,843k (± 0.02%) +30k (+ 0.01%) 199,750k 199,950k
Parse Time 1.51s (± 0.77%) 1.50s (± 0.60%) -0.01s (- 0.86%) 1.48s 1.52s
Bind Time 1.39s (± 0.64%) 1.39s (± 0.29%) -0.00s (- 0.22%) 1.38s 1.40s
Check Time 4.63s (± 0.71%) 4.56s (± 0.60%) -0.06s (- 1.36%) 4.50s 4.61s
Emit Time 3.11s (± 0.70%) 3.09s (± 0.71%) -0.02s (- 0.71%) 3.02s 3.12s
Total Time 10.65s (± 0.48%) 10.54s (± 0.53%) -0.10s (- 0.95%) 10.40s 10.65s
TFS - node (v8.9.0, x86)
Memory used 175,875k (± 0.02%) 175,950k (± 0.02%) +75k (+ 0.04%) 175,858k 175,993k
Parse Time 1.20s (± 0.67%) 1.21s (± 0.83%) +0.01s (+ 0.75%) 1.19s 1.24s
Bind Time 1.23s (± 0.42%) 1.23s (± 0.94%) +0.00s (+ 0.32%) 1.21s 1.26s
Check Time 4.03s (± 0.38%) 4.06s (± 0.74%) +0.03s (+ 0.82%) 4.00s 4.10s
Emit Time 2.77s (± 0.91%) 2.76s (± 1.22%) -0.02s (- 0.54%) 2.69s 2.84s
Total Time 9.23s (± 0.36%) 9.26s (± 0.56%) +0.03s (+ 0.30%) 9.18s 9.44s
Angular - node (v9.0.0, x64)
Memory used 330,480k (± 0.02%) 330,500k (± 0.01%) +19k (+ 0.01%) 330,394k 330,588k
Parse Time 1.64s (± 0.61%) 1.64s (± 0.32%) -0.01s (- 0.30%) 1.63s 1.65s
Bind Time 1.34s (± 0.68%) 1.34s (± 0.81%) +0.00s (+ 0.07%) 1.32s 1.37s
Check Time 4.33s (± 0.53%) 4.28s (± 0.48%) -0.05s (- 1.25%) 4.25s 4.35s
Emit Time 5.85s (± 1.78%) 5.69s (± 1.35%) -0.16s (- 2.75%) 5.61s 5.97s
Total Time 13.17s (± 0.87%) 12.95s (± 0.56%) -0.22s (- 1.69%) 12.88s 13.20s
Monaco - node (v9.0.0, x64)
Memory used 358,239k (± 0.01%) 358,201k (± 0.01%) -38k (- 0.01%) 358,141k 358,312k
Parse Time 1.30s (± 0.54%) 1.29s (± 0.31%) -0.01s (- 0.62%) 1.28s 1.30s
Bind Time 1.51s (± 0.62%) 1.52s (± 0.77%) +0.01s (+ 0.40%) 1.50s 1.55s
Check Time 4.70s (± 0.47%) 4.64s (± 0.36%) -0.06s (- 1.21%) 4.60s 4.67s
Emit Time 3.23s (± 0.40%) 3.24s (± 0.41%) +0.01s (+ 0.28%) 3.21s 3.26s
Total Time 10.73s (± 0.29%) 10.68s (± 0.27%) -0.05s (- 0.45%) 10.62s 10.74s
TFS - node (v9.0.0, x64)
Memory used 313,774k (± 0.02%) 313,797k (± 0.01%) +23k (+ 0.01%) 313,690k 313,893k
Parse Time 1.02s (± 0.57%) 1.02s (± 0.69%) +0.00s (+ 0.20%) 1.00s 1.03s
Bind Time 1.21s (± 1.34%) 1.21s (± 0.73%) 0.00s ( 0.00%) 1.20s 1.24s
Check Time 4.25s (± 1.98%) 4.10s (± 1.28%) -0.15s (- 3.56%) 4.04s 4.29s
Emit Time 2.98s (± 3.03%) 3.07s (± 1.95%) +0.09s (+ 2.85%) 2.85s 3.17s
Total Time 9.46s (± 0.50%) 9.39s (± 0.32%) -0.07s (- 0.72%) 9.31s 9.44s
Angular - node (v9.0.0, x86)
Memory used 187,355k (± 0.02%) 187,359k (± 0.02%) +4k (+ 0.00%) 187,240k 187,444k
Parse Time 1.57s (± 1.31%) 1.56s (± 0.79%) -0.01s (- 0.57%) 1.54s 1.60s
Bind Time 1.53s (± 1.09%) 1.51s (± 0.63%) -0.01s (- 0.85%) 1.49s 1.53s
Check Time 4.06s (± 0.67%) 3.98s (± 0.36%) -0.07s (- 1.85%) 3.95s 4.01s
Emit Time 5.34s (± 0.94%) 5.33s (± 0.79%) -0.01s (- 0.13%) 5.26s 5.44s
Total Time 12.50s (± 0.76%) 12.39s (± 0.43%) -0.10s (- 0.83%) 12.29s 12.50s
Monaco - node (v9.0.0, x86)
Memory used 199,933k (± 0.04%) 199,959k (± 0.03%) +26k (+ 0.01%) 199,820k 200,086k
Parse Time 1.33s (± 0.42%) 1.33s (± 0.52%) -0.00s (- 0.23%) 1.31s 1.34s
Bind Time 1.36s (± 0.45%) 1.36s (± 0.50%) +0.01s (+ 0.37%) 1.35s 1.38s
Check Time 4.50s (± 0.60%) 4.43s (± 0.60%) -0.06s (- 1.42%) 4.40s 4.52s
Emit Time 3.02s (± 0.62%) 3.02s (± 0.54%) -0.01s (- 0.17%) 2.98s 3.05s
Total Time 10.21s (± 0.37%) 10.14s (± 0.37%) -0.06s (- 0.64%) 10.08s 10.27s
TFS - node (v9.0.0, x86)
Memory used 175,968k (± 0.02%) 176,047k (± 0.02%) +79k (+ 0.04%) 175,978k 176,097k
Parse Time 1.04s (± 0.50%) 1.04s (± 0.38%) 0.00s ( 0.00%) 1.03s 1.05s
Bind Time 1.23s (± 0.97%) 1.22s (± 1.26%) -0.01s (- 0.73%) 1.20s 1.27s
Check Time 3.91s (± 0.48%) 3.85s (± 0.49%) -0.06s (- 1.53%) 3.80s 3.89s
Emit Time 2.70s (± 0.92%) 2.71s (± 0.33%) +0.01s (+ 0.30%) 2.69s 2.73s
Total Time 8.88s (± 0.43%) 8.81s (± 0.27%) -0.06s (- 0.69%) 8.78s 8.88s
System
Machine Namets-ci-ubuntu
Platformlinux 4.4.0-142-generic
Architecturex64
Available Memory16 GB
Available Memory1 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v6.5.0, x64)
  • node (v6.5.0, x86)
  • node (v8.9.0, x64)
  • node (v8.9.0, x86)
  • node (v9.0.0, x64)
  • node (v9.0.0, x86)
Scenarios
  • Angular - node (v6.5.0, x64)
  • Angular - node (v6.5.0, x86)
  • Angular - node (v8.9.0, x64)
  • Angular - node (v8.9.0, x86)
  • Angular - node (v9.0.0, x64)
  • Angular - node (v9.0.0, x86)
  • Monaco - node (v6.5.0, x64)
  • Monaco - node (v6.5.0, x86)
  • Monaco - node (v8.9.0, x64)
  • Monaco - node (v8.9.0, x86)
  • Monaco - node (v9.0.0, x64)
  • Monaco - node (v9.0.0, x86)
  • TFS - node (v6.5.0, x64)
  • TFS - node (v6.5.0, x86)
  • TFS - node (v8.9.0, x64)
  • TFS - node (v8.9.0, x86)
  • TFS - node (v9.0.0, x64)
  • TFS - node (v9.0.0, x86)
Benchmark Name Iterations
Current 30520 10
Baseline master 10

@@ -7151,9 +7151,80 @@ namespace ts {
stringIndexInfo = intersectIndexInfos(stringIndexInfo, getIndexInfoOfType(t, IndexKind.String));
numberIndexInfo = intersectIndexInfos(numberIndexInfo, getIndexInfoOfType(t, IndexKind.Number));
}
callSignatures = intersectCallSignatures(callSignatures);
Copy link
Member

@weswigham weswigham Apr 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't call this quite right. This has lost the nuance of which type the signatures originally came from. Say I wrote:

type WhyTho = {
  (): {x;};
  (): {y;};
} & {prop;}

with this logic, when I called something of type WhyTho, the result would be of type {x;} & {y;}, even though at no point did I actually intersect multiple callable things. Like our recent union calling infrastructure, this needs to operate pairwise over the input overload lists, and only merge between types. Eg, if I write

type Another = {
  (first: number): {x;};
  (): {y;};
  (): {q;};
} & {
  (): {x;}
} & {
  (arg: number): {z;}
}

I expect to see a resulting overload list along the lines of

  (first: number): {x;} & {z;};
  (): {y;} & {x;};
  (): {q;} & {x;};

it'd be a different story if for overload resolution we always resolved all applicable overloads "simultaneously" and merged the result, but unfortunately we always pick the first applicable overload, so retaining overload list order is important to retaining consistent behavior.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking a look @weswigham! I thought of that while implementing this, but I couldn't think of a way that a function could satisfy the the type WhyTho without also satisfying the intersected form produced by my change. For example:

type WhyTho = {
    (): {x;};
    (): {y;};
  } & {prop;}

function asdf() {
    return {
        x: 1
    };
}

asdf.prop = 4;

const whatever: WhyTho = asdf;

The error on the last line is:

Type 'typeof asdf' is not assignable to type 'WhyTho'.
  Type 'typeof asdf' is not assignable to type '{ (): { x: any; }; (): { y: any; }; }'.
    Property 'y' is missing in type '{ x: number; }' but required in type '{ y: any; }'.ts(2322)

How can that error by resolved other than by changing asdf to return the intersected type? I think that's why most languages I'm familiar with consider it an error in the first place to have overloads that differ only be return type.

Still, I agree it's undesirable that the signature changes just because of an unrelated intersection. Assuming it wouldn't be acceptable to always intersect return types of signatures with identical parameters (even in the absence of an explicit intersection), I'll see if I can change the code to only intersect return types of signatures from different intersected types. Any advice in that regard is greatly appreciated of course!

Copy link
Author

@kring kring Apr 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer my own question:

How can that error by resolved other than by changing asdf to return the intersected type?

It can be resolved by declaring an overload, of course!

function asdf(): {x:any};
function asdf(): {y:any};
function asdf(): any {
    return 'but what should this value really be?';
}

It kind of makes my brain hurt to figure out what those two overloads with identical parameters are supposed to tell us about that function. It sort of just seems like it should be an error, but I'm sure there are good reasons that it's not.

So moving on, I'm implementing the change to only intersect return types across types, never within a type, and it seems mostly straightforward so far. I just want to clarify what should happen in a slightly more complicated situation than the one you mentioned:

type Another = {
  (first: number): {x;};
  (): {y;};
  (): {q;};
} & {
  (): {x;}
  (): {r;} // this line added
} & {
  (arg: number): {z;}
}

I think in this case, it should create no-parameters signatures which are the Cartesian product of the no-parameters signatures in the intersected types. So:

  (first: number): {x;} & {z;};
  (): {y;} & {x;};
  (): {y;} & {r;};
  (): {q;} & {x;};
  (): {q;} & {r;};

Does that seem right to you?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in this case, it should create no-parameters signatures which are the Cartesian product of the no-parameters signatures in the intersected types.

I agree with the premise, but also disagree with doing it for the same reason we don't combine multiple multi-overload sets when manufacturing union signatures: overload order matters, and the "correct" ordering of the interleaved product of signatures is ambiguous. So long as overload order matters, we can't actually combine multiple ordered lists (since the only "correct" behavior would be to resolve on each type of the union/intersection separately and combine the single selected signature).

Copy link
Author

@kring kring May 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been giving this a fair bit of thought over the past few days, and, if I'm understanding you correctly, we need to make sure that any change to signature intersection follows these rules.

Upon intersection:

  1. the overloads within any of the constituent types of the intersection cannot be combined / have their return types intersected, even if they differ only be return type.
  2. the overloads within any of the constituent types of the intersection cannot change order relative to one another.

In addition, there may be one more rule:
3. The overloads of an intersection type must occur in the order of intersection of their source types.

If all three of these rules must hold, the result of an intersection will have to be pretty much as it is today, except that if two overloads that differ only by return type happen to be right next to each other in the list (as currently computed in master), and they come from different types, then they can be turned into a single overload by intersecting their return types.

So in this example:

type Another = {
  (first: number): {x;}
  (): {y;}
  (): {q;}
} & {
  (): {x;}
  (): {r;}
} & {
  (arg: number): {z;}
}

The result would be:

(first: number): {x;}
(): {y;}
(): {q;} & {x;}
(): {r;}
(arg: number): {z;}

However, if we can relax rule 3, we could achieve something like I think you were getting at with this statement:

the only "correct" behavior would be to resolve on each type of the union/intersection separately and combine the single selected signature

If I'm understanding you correctly, you're saying the only correct algorithm would be this: given a function call on an intersected type, we perform overload resolution on all of the constituent intersected types independently. Some may resolve successfully, some may not. Put all the successful ones in a list in the same order as the types they came from. Then, select the first signature from the list and intersect its return type with all other signatures in the list that differ from the first one only be return type.

That won't really uphold rule 3. Given the type above and this call site:

const f: Another = ...;
f();

We select (): {y;} from the first type in the intersection and (): {x;} from the second type, so the intersected signature would be (): {y;} & {x;}. That puts precedence on (): {x;} from the second type over (): {q;} from the first type, violating rule 3.

But if that's ok (I think it is! It's less surprising than the (): {y;} we get when we strictly enforce rule 3), then we can achieve the same effect as this call-site algorithm at intersection-time with an algorithm like this:

for all types T in intersection {
  for all _remaining_ signatures S in type T {
    signature R = S
    for all types T' in the intersection that _follow_ T {
      for all _remaining_ signatures S' in type T' {
        if S is identical to S' (ignoring return type) {
          change the return type of R to the intersection of the return types of R and S'
          remove S' from consideration in the T/T' signature set
          break loop over signatures of T'
        }
      }
    }
    add overload R to result
  }
}         

Which for the type above would yield:

{
  (first: number): {x;} & {z;}
  (): {y;} & {x;}
  (): {q;} & {r;}
}

Which seems quite sensible to me.

Does that make sense to you?

I've been studying your recent union changes (#29011), trying to understand how that approach would apply to the intersection case. I've learned a lot about how to actually compare the signatures, but less obvious is how the intersection should behave.

By the way, I find this interesting and am happy to keep working on it even if that involves a bunch of false starts and iteration. Do let me know if I'm just wasting your time, though. There's no point in doing that. :)

@RyanCavanaugh RyanCavanaugh added the Experiment A fork with an experimental idea which might not make it into master label Apr 30, 2019
@sandersn
Copy link
Member

sandersn commented Feb 3, 2020

Like a lot of the Experiment-labelled PRs, this one is hard to push any further without an associated proposal that we can discuss. It looks like the best place is #9239, which is indeed missing a complete proposal.

@sandersn
Copy link
Member

This experiment is pretty old, so I'm going to close it to reduce the number of open PRs.

@sandersn sandersn closed this May 24, 2022
@typescript-bot
Copy link
Collaborator

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experiment A fork with an experimental idea which might not make it into master
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants