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

Control flow analysis for dependent parameters #47190

Merged
merged 2 commits into from
Jan 4, 2022
Merged

Control flow analysis for dependent parameters #47190

merged 2 commits into from
Jan 4, 2022

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Dec 18, 2021

This PR builds on the work in #46266 to support control flow analysis for dependent parameters contextually typed by a rest parameter with a discriminated union type. Some examples:

type Args = ['a', number] | ['b', string];

const f1: (...args: ['a', number] | ['b', string]) => void = (kind, payload) => {
    if (kind === 'a') {
        payload.toFixed();  // payload narrowed to number
    }
    if (kind === 'b') {
        payload.toUpperCase();  // payload narrowed to string
    }
};

f1('a', 42);
f1('b', 'hello');

declare function f2(cb: (...args: Args) => void): void

f2((kind, data) => {
    if (kind === 'a') {
        data.toFixed();  // data narrowed to number
    }
    if (kind === 'b') {
        data.toUpperCase();  // data narrowed to string
    }
});

type ReadFileCallbackArgs =
    | [err: undefined, data: unknown[]]
    | [err: Error, data: undefined];

declare function readFile(path: string, cb: (...args: ReadFileCallbackArgs) => void): void;

readFile('hello', (err, data) => {
    if (!err) {
        data.length;  // data narrowed to unknown[]
    }
    else {
        err.message;  // err narrowed to Error
    }
});

Fixes #46658.

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Dec 18, 2021
@ahejlsberg
Copy link
Member Author

@typescript-bot perf test faster
@typescript-bot test this
@typescript-bot user test this inline
@typescript-bot run dt

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 18, 2021

Heya @ahejlsberg, I've started to run the parallelized Definitely Typed test suite on this PR at 35f7909. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 18, 2021

Heya @ahejlsberg, I've started to run the abridged perf test suite on this PR at 35f7909. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 18, 2021

Heya @ahejlsberg, I've started to run the inline community code test suite on this PR at 35f7909. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 18, 2021

Heya @ahejlsberg, I've started to run the extended test suite on this PR at 35f7909. You can monitor the build here.

Copy link
Member

@weswigham weswigham left a comment

Choose a reason for hiding this comment

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

A test showing how this behavior is disabled when one of the parameters is reassigned would be nice.

With this, would we revisit giving overload implementation parameters a contextual type equal to the union of the overload parameter lists (under noImplicitAny), so overloads can make use of this without kinda contorting your function definition style?

@typescript-bot
Copy link
Collaborator

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

Here they are:

Comparison Report - main..47190

Metric main 47190 Delta Best Worst
Angular - node (v14.15.1, x64)
Memory used 331,303k (± 0.01%) 331,237k (± 0.01%) -66k (- 0.02%) 331,113k 331,318k
Parse Time 1.95s (± 0.30%) 1.95s (± 0.64%) +0.00s (+ 0.15%) 1.92s 1.98s
Bind Time 0.86s (± 0.55%) 0.86s (± 0.52%) +0.00s (+ 0.23%) 0.85s 0.87s
Check Time 5.43s (± 0.37%) 5.40s (± 0.37%) -0.02s (- 0.42%) 5.37s 5.46s
Emit Time 6.20s (± 0.72%) 6.22s (± 0.81%) +0.03s (+ 0.45%) 6.14s 6.35s
Total Time 14.43s (± 0.44%) 14.44s (± 0.50%) +0.01s (+ 0.07%) 14.32s 14.63s
Compiler-Unions - node (v14.15.1, x64)
Memory used 190,917k (± 0.51%) 191,260k (± 0.57%) +344k (+ 0.18%) 190,233k 193,542k
Parse Time 0.81s (± 0.90%) 0.81s (± 0.71%) +0.00s (+ 0.12%) 0.80s 0.83s
Bind Time 0.55s (± 0.81%) 0.56s (± 0.67%) +0.00s (+ 0.54%) 0.55s 0.56s
Check Time 7.40s (± 0.59%) 7.47s (± 0.77%) +0.07s (+ 0.92%) 7.35s 7.62s
Emit Time 2.47s (± 0.53%) 2.48s (± 0.62%) +0.00s (+ 0.20%) 2.45s 2.52s
Total Time 11.24s (± 0.39%) 11.31s (± 0.60%) +0.08s (+ 0.68%) 11.18s 11.51s
Monaco - node (v14.15.1, x64)
Memory used 324,408k (± 0.00%) 324,456k (± 0.01%) +48k (+ 0.01%) 324,427k 324,508k
Parse Time 1.50s (± 0.55%) 1.51s (± 0.65%) +0.00s (+ 0.27%) 1.49s 1.54s
Bind Time 0.76s (± 0.85%) 0.76s (± 0.65%) -0.00s (- 0.26%) 0.75s 0.77s
Check Time 5.33s (± 0.41%) 5.39s (± 0.71%) +0.07s (+ 1.26%) 5.34s 5.52s
Emit Time 3.25s (± 0.48%) 3.27s (± 0.69%) +0.02s (+ 0.52%) 3.24s 3.34s
Total Time 10.84s (± 0.26%) 10.92s (± 0.50%) +0.09s (+ 0.81%) 10.83s 11.08s
TFS - node (v14.15.1, x64)
Memory used 289,202k (± 0.00%) 289,245k (± 0.01%) +44k (+ 0.02%) 289,196k 289,285k
Parse Time 1.23s (± 0.49%) 1.23s (± 0.83%) +0.00s (+ 0.33%) 1.21s 1.25s
Bind Time 0.73s (± 0.84%) 0.73s (± 0.91%) -0.00s (- 0.14%) 0.72s 0.75s
Check Time 4.97s (± 0.43%) 4.99s (± 0.61%) +0.02s (+ 0.36%) 4.93s 5.04s
Emit Time 3.51s (± 0.48%) 3.49s (± 1.41%) -0.03s (- 0.74%) 3.31s 3.56s
Total Time 10.44s (± 0.38%) 10.43s (± 0.69%) -0.01s (- 0.07%) 10.18s 10.53s
material-ui - node (v14.15.1, x64)
Memory used 448,417k (± 0.00%) 448,338k (± 0.04%) -79k (- 0.02%) 447,651k 448,449k
Parse Time 1.82s (± 0.37%) 1.83s (± 0.75%) +0.01s (+ 0.55%) 1.81s 1.86s
Bind Time 0.68s (± 0.54%) 0.68s (± 0.50%) +0.00s (+ 0.15%) 0.67s 0.68s
Check Time 12.91s (± 0.51%) 12.92s (± 0.87%) +0.02s (+ 0.13%) 12.72s 13.19s
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) 0.00s ( NaN%) 0.00s 0.00s
Total Time 15.41s (± 0.44%) 15.43s (± 0.77%) +0.02s (+ 0.16%) 15.20s 15.73s
xstate - node (v14.15.1, x64)
Memory used 532,865k (± 0.00%) 532,896k (± 0.00%) +32k (+ 0.01%) 532,850k 532,922k
Parse Time 2.56s (± 0.46%) 2.57s (± 0.58%) +0.01s (+ 0.35%) 2.54s 2.60s
Bind Time 1.14s (± 0.66%) 1.15s (± 1.13%) +0.01s (+ 0.61%) 1.13s 1.19s
Check Time 1.47s (± 0.38%) 1.48s (± 0.64%) +0.01s (+ 0.82%) 1.47s 1.51s
Emit Time 0.07s (± 0.00%) 0.07s (± 0.00%) 0.00s ( 0.00%) 0.07s 0.07s
Total Time 5.25s (± 0.30%) 5.28s (± 0.42%) +0.03s (+ 0.53%) 5.23s 5.33s
System
Machine Namets-ci-ubuntu
Platformlinux 4.4.0-210-generic
Architecturex64
Available Memory16 GB
Available Memory8 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v14.15.1, x64)
Scenarios
  • Angular - node (v14.15.1, x64)
  • Compiler-Unions - node (v14.15.1, x64)
  • Monaco - node (v14.15.1, x64)
  • TFS - node (v14.15.1, x64)
  • material-ui - node (v14.15.1, x64)
  • xstate - node (v14.15.1, x64)
Benchmark Name Iterations
Current 47190 10
Baseline main 10

Developer Information:

Download Benchmark

@typescript-bot
Copy link
Collaborator

@ahejlsberg
Great news! no new errors were found between main..refs/pull/47190/merge

@Andarist
Copy link
Contributor

Andarist commented Dec 25, 2021

I was wondering if this could help us with some XState types and I've created a more complex test case that resembles how we would like to utilize this feature (which is awesome!) but unfortunately, it turns out that this doesn't work for us.

I believe there is a potential for refining the context parameter here in a way that context.user would not include null as part of its type:
44d9f91#diff-2f166985e9669bbcb575d1ceff7375d792893917411711533a640c0cee0c15a7R232-R244

Is this something that could be addressed in this PR? Is making this work aligning with your goals for this feature? Should this be tracked separately?

EDIT:// after looking more into this - this is actually out of the scope of this PR since simpler situations (with regular object/tuple structures) don't work either:
TS playground

It's because x can't be narrowed down based on x.event.type check. I'll look if there is any issue about this (I bet there is) to comment there with our use case. Would you say that it's possible to support this one day? Or do you predict that this might not be feasible due to perf constraints or something else?

Just as a note - with a dedicated structure it's at least possible to narrow this situation down with custom type predicated (like here) but it would be awesome if this would just work when using language features. And, of course, this approach (custom type predicates) won't work with dependent parameters at all.

@ahejlsberg ahejlsberg merged commit f4e1efb into main Jan 4, 2022
@ahejlsberg ahejlsberg deleted the fix46658 branch January 4, 2022 19:22
@ahejlsberg
Copy link
Member Author

@Andarist The issue in your example is that the discriminant is stored in a nested object instead of directly in a property. In other words, this doesn't work:

type XStateActionArgs =
  | [event: { type: "LOG_OUT" }, context: { user: { name: string } }]
  | [event: { type: "LOG_IN" }, context: { user: null }];

whereas this does:

type XStateActionArgs =
  | [event: "LOG_OUT", context: { user: { name: string } }]
  | [event: "LOG_IN", context: { user: null }];

Effectively, discriminants only affect the directly containing type. This is true for discriminated unions in general, and isn't specific to this PR.

@KamenKolev
Copy link

KamenKolev commented Jan 22, 2022

I'm not sure if this is a bug, or out of scope but it seems to me like something like this should work

const fn: (...args: [number, number] | [string, string]) => void = (a, b) => {
  if (typeof a === "number") {
    console.log(a + b); // 'b' still 'string | number'
  }
}

What could be the reason for this to fail?

@Jackie1210
Copy link

I'm not sure if this is a bug, or out of scope but it seems to me like something like this should work

const fn: (...args: [number, number] | [string, string]) => void = (a, b) => {
  if (typeof a === "number") {
    console.log(a + b); // 'b' still 'string | number'
  }
}

What could be the reason for this to fail?

I came out the same issue. Did you resolve it? @KamenKolev

@Andarist
Copy link
Contributor

I'm not 100% sure but I think that this only works for discriminated unions and a discriminated union mandates that you are using a literal member~. string, nor number, are literals so this doesn't work.

@jcalz
Copy link
Contributor

jcalz commented Mar 3, 2022

Hmm, is there a reason this doesn't work for methods?

type Foo = {
  method(...args:
    [type: "str", cb: (e: string) => void] |
    [type: "num", cb: (e: number) => void]
  ): void;
}

// this fails for some reason, as a method
let fooBad: Foo = {
  method(type, cb) {
    if (type == 'num') {
      cb(123) // error!
    } else {
      cb("abc") // error!
    }
  }
};

// suddenly works for arrow function
let fooOkay1: Foo = {
  method: (type, cb) => {
    if (type == 'num') {
      cb(123)
    } else {
      cb("abc")
    }
  }
};

// also works for manual destructuring of rest args
let fooOkay2: Foo = {
  method(...args) {
    const [type, cb] = args;
    if (type == 'num') {
      cb(123)
    } else {
      cb("abc")
    }
  }
};

Playground link

(note to self, follow up on this SO question)

I guess if I don't hear back here I can open a new issue

@Andarist
Copy link
Contributor

Andarist commented Mar 3, 2022

For the record - I'm in the process of debugging this issue ☝️ . I'm not sure if I will be able to actually fix it - but I'm trying 😉 I already have debugged this to the very exact code line where the behavior is diverging for those cases.

EDIT:// I think I have a fix locally - gonna prepare a PR today or tomorrow morning.

@jcalz
Copy link
Contributor

jcalz commented Mar 5, 2022

We might need to open a new issue for this and then see if the TS team would even accept a PR for it.

@Andarist
Copy link
Contributor

Andarist commented Mar 5, 2022

The PR is already up: #48110

We can go through a ceremony of opening an issue and attaching it to this PR but from my PoV that would just be redundant bureaucracy

@Andarist
Copy link
Contributor

Andarist commented Mar 5, 2022

I think this issue is something entirely else. I'm also not sure if this is an issue if we consider how TS is designed. You are operating here on IBase but this type has no relation to IFoo and IBar, so if you "refine" it then the compiler won't recognize that your intention was to pick one of the union members since that implied union doesn't really exist anywhere here. This looks fine to me if we consider that IFoo and IBar have different types for a value property than IBase.

However, it seems that perhaps this TS playground could actually work 🤔 Either way, I would classify this as a totally separate issue - cause knowing what this PR was about this is something entirely else.

Take my words with a grain of salt - I'm just a bystander here 😅

@jcalz
Copy link
Contributor

jcalz commented Dec 6, 2022

Is there a reason this doesn't work if the tuples in the union are of different lengths?

type Foo = (...args: [type: "one"] | [type: "two", x: string]) => void;
const foo: Foo = (type, x) => { // error!
  if (type !== "one") x.toUpperCase();
}

Or potentially of different lengths?

type Foo = (...args: [type: "one", x?: number] | [type: "two", x: string]) => void;
const foo: Foo = (type, x) => { // error!
  if (type !== "one") x.toUpperCase();
}

Playground link to code

Shall I file this as a new issue or is there something I'm missing?

@Andarist
Copy link
Contributor

Andarist commented Dec 6, 2022

@jcalz this looks like the issue reported here. I actually have a partial fix here but need to cover more cases there - I guess it might be the time to revive that PR on my side 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Control flow analysis for dependent function parameters "destructured" from a tuple type
9 participants