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

Fix 21732: "in" operator could widden type #39746

Closed

Conversation

ShuiRuTian
Copy link
Contributor

Fixes #21732

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Jul 25, 2020
@ShuiRuTian
Copy link
Contributor Author

ShuiRuTian commented Jul 25, 2020

Some questions would stop PR from merging in my thought. Have no idea about these for now, I would add new comments when idea comes.

  1. The code structure
    This PR would let "in" operator has the ability to add something to a type. In other words, it would "expand" or "widden" a type rather than "narrow" it.
    Then, is it acceptable or toleratable to has one branch like narrowOrWiddenTypeByInOperator in function narrowType?
    If not, what should the code structure be like?

}
return type;

// I would be very glad to create a helper file like `nodeTests.ts` if feedback positive review.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, what about a helper file for Type to narrow their type?

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

There are a lot of properties that the control flow code needs to maintain that this change will violate. A full change will involve quite a lot of change to the control flow code, not just the narrowing code.

The best place to start is to add tests for in in complex control flows, like if or switch nested in while. Look at the existing control flow tests for examples.

@ShuiRuTian
Copy link
Contributor Author

ShuiRuTian commented Sep 4, 2020

Oh my.....

Thanks a lot to the review and suggestion, I find a simple example that works wrongly.

var tmp = { a: 1, b: "foo" }
tmp; // add "e" to the type.
if ("d" in tmp) {
    tmp;
    if ("e" in tmp) {
        tmp;
    }
    tmp;
}

I would have a try to change CFA, but it is great if there is any help to explain how does it works briefly, I am totally new on this area.

-- edit:
I found this belongs to immutable things rather than CFA, and it seems work well now, just like other narrow statements!

I would continue check, but it would still be very helpful if there is an error example to analytics on CFA.

@Kingwl
Copy link
Contributor

Kingwl commented Sep 9, 2020

You can see __debugFlowFlags property in flowNode. That shows how the data flow works for the current code.

@ShuiRuTian
Copy link
Contributor Author

ShuiRuTian commented Sep 9, 2020

@Kingwl Thanks for the kind help!

And...... could I ask a question? ---- What do you think of the issue? Why this PR need to change CFA?
I think 'in' operator just works like other narrow statement (like discriminant), if it has CFA issue, it should already have.

@sandersn
Copy link
Member

@ShuiRuTian do you want to keep working on this?

@ShuiRuTian
Copy link
Contributor Author

@sandersn Sure, but I think I need feedback -- Why this PR need to change control flow?
I think 'in' operator just works like other narrow statements (like discriminant, instanceof, typeof ...), if it has an CFA issue, it should already have.

And I do test some cases, it seems they work correctly.

@sandersn
Copy link
Member

@weswigham can you take a look and give some direction on this?

@ShuiRuTian
Copy link
Contributor Author

@weswigham Sorry for bothering, could you help this PR find direction when have spare time? It lost at the sea....

@ShuiRuTian
Copy link
Contributor Author

up!

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.

The key issue we normally flag when thinking about this is branch reconciliation. Take something like this:

var tmp = { a: 1, b: "foo" };
if ("c" in tmp) {
    tmp;
}
else if ("d" in tmp) {
    tmp;
}
else {
    return;
}
tmp; // should this be `{a: number, b: string}` or `{a: number, b: string, c: unknown} | {a: number, b: string, d: unknown}`

and then there's the question of what happens when you both discriminate and "expand":

declare var x: {type: "a", f1: string} | {type: "b", f2: string};
if (x.type === "a") {
  if ("f2" in x) {
    x; // is this `never`? is this `{type: "a", f1: string, f2: unknown}` ?
  }
}
x; // needs to have gone back to `{type: "a", f1: string} | {type: "b", f2: string}` here, and not be something wonky like `({type: "a", f1: string, f2: unknown}) | {type: "b", f2: string}`

const members = createSymbolTable();
members.set(propName, newSymbol);
if (widenedType.members) {
mergeSymbolTable(members, widenedType.members);
Copy link
Member

Choose a reason for hiding this comment

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

This recursively merges the symbol table deeply (so overlapping symbols within the table get combined, and have a merge pointer at the new, combined symbol). You don't want that. Rather than most of this logic, you should probably just use getSpreadType, rather than everything after the // if type is intersection comment, since it already combines object types where appropriate, since you guarantee you're adding a new property in the second object anyway. (Re)Using it should also allow you to cut out a lot of the other structural changes around intersection construction in this PR, too.

@ShuiRuTian
Copy link
Contributor Author

Thanks for the review and suggestion!

Here are the types this PR could get:
PS: In this PR(for now), use intersection rather than just "add" into origional type. which does not influence us discussing about behaviour, right?

function f(){
    var tmp = { a: 1, b: "foo" };
    if ("c" in tmp) {
        tmp; // {a: number;b: string;} & {c: unknown;}
    }
    else if ("d" in tmp) {
        tmp; // {a: number;b: string;} & {d: unknown;}
    }
    else {
        return;
    }
    tmp; // ({a: number;b: string;} & {c: unknown;}) | ({a: number;b: string;} & {d: unknown;})
}

declare var x: {type: "a", f1: string} | {type: "b", f2: string};
if (x.type === "a") {
  if ("f2" in x) {
    x; // {type: "a";f1: string;} & {f2: unknown;}
  }
}
x; // {type: "a";f1: string;} | {type: "b";f2: string;}

I was confused how to make choice between the commnets in the example code. It seems both are reasonable to some degree for me. And finally I decide to keep it just same with other narrow statements, at least it is not a bad decision in my thought, it keeps consistant.

However, I am not sure whether it is good choice on earth. My concern might be simple, sometimes navie. What do you think?

PS: need a little time to get getSpreadType into use, trying!

@sandersn
Copy link
Member

@ShuiRuTian do you want to keep working on this?

@sandersn
Copy link
Member

I'm going to close this PR to reduce clutter. Let me know if you want to restart work on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Suggestion: treat in operator as type guard which asserts property existence
5 participants