-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Check for missing property on union type causes failure in subsequent property checks #58448
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
Comments
This is a consequence of how control flow causes subtype reduction. It might be fixable but the |
So there are certain types of control flow analyses that can alter the type beyond the end of a (non-returning) block? Is that necessary for certain checks/behaviors or just a side effect of the current implementation? We have a workaround for this issue, so it isn't critical at the moment that this is fixed upstream. That being said I'd still be interested in a solution. |
The basic pattern here is whenever control flow has two different ways to get to the same place, it's going to union the upstream types, and that unioning is subject to reduction. So in the most basic example function do_something() {
const comp = {} as comp;
comp;
// ^?
if ("c" in comp) {
}
// --> here <--
comp;
// ^?
} At Without reduction, you'd see weird artifacts, e.g. function foo(x: Animal) {
if (isCat(x)) { /* no-op */ }
// --> here <--
} In this example, at |
Thanks for elucidating the behavior of the incoming types, but I'm still not quite clear why |
Subtype reduction doesn't treat |
So the subtype reduction of That feels wrong somehow, but I see now what you mean that these kinds of types will have issues due to this reduction. |
const namedUnions: Type[] = [];
addNamedUnions(namedUnions, types);
const reducedTypes: Type[] = [];
for (const t of typeSet) {
if (!some(namedUnions, union => containsType((union as UnionType).types, t))) {
reducedTypes.push(t);
}
}
if (!aliasSymbol && namedUnions.length === 1 && reducedTypes.length === 0) {
return namedUnions[0];
} Looking at checker.ts, inside function addNamedUnions(namedUnions: Type[], types: readonly Type[]) {
for (const t of types) {
if (t.flags & TypeFlags.Union) {
const origin = (t as UnionType).origin;
if (t.aliasSymbol || origin && !(origin.flags & TypeFlags.Union)) {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pushIfUnique(namedUnions, t);
}
else if (origin && origin.flags & TypeFlags.Union) {
addNamedUnions(namedUnions, (origin as UnionType).types);
}
}
}
}
Footnotes
|
I just want to chime in to note that subtype reduction isn't only a mitigation for design constraints (as Ryan's comment might have implied); it's also the type theoretically correct thing to do: Given Union and intersection types have very interesting type theoretical implications. |
If it's the type theoretically correct thing to do, then why doesn't it subtype reduce after the |
As @ahejlsberg said, in a perfect world subtype reduction would always happen. However as a matter of practicality (and as I noted in the other issue, pragmatism), it's only done in certain cases. The behavior you describe is likely explained by #53425 (comment). |
Also relevant: #53425 (comment) |
This explains my first example, as
Hypothetical: so wouldn't it be a reasonable performance benefit to skip subtype reduction if we're at the end of a conditional block that did not return? I understand that there is a high-level "type theory" aspect to this issue, but clearly a line is drawn at some arbitrary point for performance/usability reasons. This subtype reduction doesn't always happen, and as you (@fatcerberus) pointed out here, when it does/doesn't can be observable. In this case, the observability directly results in an unexpected and confusing type error. I'm hopeful that since this was tagged with |
Yep! See e.g. #58013 |
Thanks! Looks like there's some renewed interest in this area recently. As I mentioned earlier, this isn't a blocking issue for us, so for now thanks for all of the useful info. |
The "named unions" logic is a red herring. It only has to do with computing the origin type of a union, a de-normalized representation we keep around solely for type display purposes.
In your second example,
I'm not sure what you mean by that. Subtype reduction potentially happens when two or more control flows join following a conditional construct. For example, at the bottom of an |
My misunderstanding here was about what "foreign" meant in regards to the original type. I was thinking that foreign was determined from the type immediately before the if statement, not the absolute origin. |
Yeah, Iβm a pragmatist myself so I fully understand. I bring up the type theory implications not to be patronizing but only because people often (understandably!) assume that |
π Search Terms
"missing property", "property check", "type union", "control flow analysis"
π Version & Regression Information
*There was a minor change between 4.8 and 4.9 that changed the types but did not change the behavior: inference improved from
never
toincorrectUnionElement & Record<"key", unknown>
.β― Playground Link
https://www.typescriptlang.org/play/?ts=5.4.5#code/FASwdgLgpgTgZgQwMZQARIPYFsAOBGVAb2FVNRigQBMMwAbAT1QQC5UwBXLAI1gG4SZCtVqNU3Npx79gAX2ChIsRCnTYcAJiKDSwmvSat2XXjAHzgEBjjSZcqALw7UAHzW48zt3c0K4HMCQIEFpUGgB9AGdsKAgAC3AAcwAKAEptMjUwSIh3HEciWWZIvIFnEDhUZIAiBGrUcDz04kzMnwA6BHaIDABlCBgktIFWsgB6MdQAPQB+ZwtMiqrq7nrGn2bnNvV27m6+gaHUkdHUCem5zIWyJZqkNbAmjNOOpH3+wbAU463xydn5gpFpUanUGo8Ns9Rh0uj0PkcTqNzgCrkCbiCVg8ni0Xjs9nDDl9hr9SMjLmR5LIgA
π» Code
π Actual behavior
Every property check after the (
"c" in comp
) one fails to narrow correctly if the key is one that is only defined for some of the element types of the type union. Attempting to access the property after one of these checks results inunknown
. As well, the narrowed variable in subsequent conditionals is an arbitrary(?) union element.π Expected behavior
The conditionals that come after the (
"c" in comp
) conditional should have the same behavior as the ones before it, as they are the exact same code.Additional information about the issue
This was initially noticed in a more complicated context with assertions, that I can include here as a secondary example. This one is even more strange, as it is a check for a property that is only defined on some of the elements of the union and it blocks its own duplicate check later.
The text was updated successfully, but these errors were encountered: