-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Conversation
@typescript-bot test this |
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. |
@typescript-bot test this again |
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, |
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
what is freshness please? |
@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. |
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 toboolean
.The only literal types which do not have freshness of any kind now are, I believe, tuples.
Fixes #25474