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

Introduce boolean literal freshness #27042

Merged
merged 1 commit into from
Sep 14, 2018

Conversation

weswigham
Copy link
Member

The logical follwup to adding enum freshness last week, as we mentioned at the time. I hadn't realized that we had had issues reported which required it to be fixed, but we had, so here's the freshness implementation for booleans. It's rather straightforward since all the variants can be constructed up front. The old trueType/falseType map to the new fresh variant. The change also brings a noticeable improvement to the accuracy of CFA when multiple boolean-typed declarations are involved, as they no longer always widen irrecoverably to boolean.

The only literal types which do not have freshness of any kind now are, I believe, tuples.

Fixes #25474

@weswigham
Copy link
Member Author

@typescript-bot test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 12, 2018

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

@weswigham
Copy link
Member Author

@typescript-bot test this again

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 12, 2018

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

@@ -3731,7 +3731,7 @@ namespace ts {
Unit = Literal | UniqueESSymbol | Nullable,
StringOrNumberLiteral = StringLiteral | NumberLiteral,
/* @internal */
StringOrNumberLiteralOrUnique = StringOrNumberLiteral | UniqueESSymbol,
StringOrNumberLiteralOrUnique = StringLiteral | NumberLiteral | UniqueESSymbol,
Copy link
Member

Choose a reason for hiding this comment

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

this change doesn't seem related (and I'm not sure it's an improvement?)

Copy link
Member Author

@weswigham weswigham Sep 14, 2018

Choose a reason for hiding this comment

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

Eh, I changed it because I actually removed StringOrNumberLiteral at first because it was practically unused within our code after the change, but then added it back to avoid an API break.

const reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t));
let reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t));
if (assignedType.flags & (TypeFlags.FreshLiteral | TypeFlags.Literal)) {
reducedType = mapType(reducedType, getFreshTypeOfLiteralType); // Ensure that if the assignment is a fresh type, that we narrow to fresh types
Copy link
Member

Choose a reason for hiding this comment

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

why didn't we have to do this before, for string and number literal types?

Copy link
Member Author

@weswigham weswigham Sep 14, 2018

Choose a reason for hiding this comment

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

We probably should have been but as it turns out people narrow boolean made via !!expr || !expr a ton more than narrowing a string literal union ("" | "b") made via x === "" || x === "b", so it's a lot more obvious an issue with booleans.

Copy link
Member Author

Choose a reason for hiding this comment

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

And as I said in person, this is going to break one RWC project along the lines of this:

type RuleSeverity = "warning" | "error" | "off";
interface IOptions {
    ruleSeverity: RuleSeverity;
}

function parseRuleOptions(): Partial<IOptions> {
    let defaultRuleSeverity: RuleSeverity = "error";
    let ruleSeverity = defaultRuleSeverity;

    return {
        ruleSeverity,
    };
}

previously this worked because defaultRuleSeverity was narrowed to the nonfresh "error" when it was used to initialize ruleSeverity, whereas now we choose the fresh "error" which widens (and triggers an error in the return type below). So this is a (probably small) break.

Copy link
Member

Choose a reason for hiding this comment

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

So this change appears to be the cause of #27259. I'm really not sure I like this. The intuitive rule was always that once you add a type annotation to let, const or var declaration, values read from that variable are not fresh. But now they are unless the annotated type is a unit type.

const x1: 'x' = 'x';
let z1 = x1;  // 'x'

const x2: 'x' | 'y' = 'x';
let z2 = x2;  // Was 'x', now string

I find it hard to construct a rationale for that.

Copy link
Member Author

Choose a reason for hiding this comment

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

FWIW the individual unit type only behaves differently because it skips the narrowing codepath. Had I realized as much I probably would've altered the unit type behavior, too. In any case, removing this bit if we don't like it is easy; but it's going to make boolean literals a tad more breaky.

// (The union is cached, so simply doing the marking here is sufficient)
createBooleanType([regularFalseType, trueType]);
createBooleanType([falseType, regularTrueType]);
createBooleanType([falseType, trueType]);
Copy link
Member

Choose a reason for hiding this comment

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

can we ever get a union of falseType | regularFalseType? Note that it doesn't matter how we print that, I'm just curious whether it can happen.

Copy link
Member Author

@weswigham weswigham Sep 14, 2018

Choose a reason for hiding this comment

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

Uhmmm, I don't think so, just like how we can't have "word" | "word"(fresh edition), since both have the same type "identity". Specifically, removeRedundantLiteralTypes handles removing redundant fresh types when the regular one is also present.

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.

Looks good, although I'd like to hear back on my two questions before checking it in.

@weswigham weswigham merged commit 20eafb5 into microsoft:master Sep 14, 2018
@weswigham weswigham deleted the boolean-freshness branch September 14, 2018 22:00
@zpdDG4gta8XKpMCd
Copy link

what is freshness please?

@sandersn
Copy link
Member

@Aleksey-Bykov I wrote a novel-length treatment of the different ways the compiler converts types to related types. Note that it still doesn’t cover the base constraint type, which is related to the apparent type. But it does cover freshness pretty well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking Change Would introduce errors in existing code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Literal type false unexpectedly widens into type boolean
6 participants