Open
Description
π Search Terms
"key order when matching object types" "discriminated unions key order"
π Version & Regression Information
- This changed between versions 5.1 and 5.2
β― Playground Link
π» Code
type Food = 'apple' | 'orange';
type Vegetable = 'spinach' | 'carrot';
type Other = 'milk' | 'water';
type Custom = 'air' | 'soil';
type Target =
| {
audience: 'earth';
meal:
| Custom
| `fruit_${Food}`
| `vegetable_${Vegetable}`
| `other_${Other}`;
}
| {
audience: 'mars' | 'jupiter';
meal: string;
}
const vegetable: Vegetable = 'carrot';
// ok
const target: Target = {
audience: 'earth',
meal: `vegetable_${vegetable}`
};
// TS Error
// Type '{ meal: string; audience: "earth"; }' is not assignable to type 'Target'.
// Types of property 'audience' are incompatible.
// Type '"earth"' is not assignable to type '"mars" | "jupiter"'
const target2: Target = {
meal: `vegetable_${vegetable}`,
audience: 'earth'
};
Output
"use strict";
const vegetable = 'carrot';
const target = { // ok
audience: 'earth',
meal: `vegetable_${vegetable}`
};
const target2 = { // error
meal: `vegetable_${vegetable}`,
audience: 'earth'
};
Compiler Options
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"alwaysStrict": true,
"esModuleInterop": true,
"declaration": true,
"target": "ES2017",
"jsx": "react",
"module": "ESNext",
"moduleResolution": "node"
}
}
Playground Link: Provided
π Actual behavior
In the code both target
and target2
have the correct structure based on the Target
type.
But the order of keys is reversed in target2
which used to work but not anymore.
π Expected behavior
Both target
and target2
should be valid.
Additional information about the issue
Discriminated unions didn't rely on the key ordering AFAIK.
Activity
Andarist commentedon Jan 30, 2024
I have bisected this to #53907 . It means that this code with this change should roughly be equivalent to the one with
vegetable
variable inlined:This one doesn't error though. I'll investigate further what's the difference and what the behavior should be.
fatcerberus commentedon Jan 30, 2024
What, no tomatoes?
...oh no, they didn't turn carnivorous and go on a rampage did they? because if so... run
Andarist commentedon Jan 30, 2024
This is fun π
I'll push out a fix for this soon.
ahejlsberg commentedon Jan 30, 2024
A simpler repro that isn't fixed by #57236:
The root problem is that contextual types are narrowed by discriminant properties in the order those discriminant properties are written (as opposed to some canonical narrowing that considers all of them at the same time). Above, the assignment to
target
succeeds because the contextual typeTarget
is first narrowed by theaudience: "earth"
property. However, the assignment totarget2
fails because no narrowing as (yet) taken place, so the contextual type for the template literal isstring
.In reality,
meal
shouldn't really be considered a discriminant property since one of the property types is a supertype of every discriminant. We don't currently reason about it that way, but I'll look into a PR for that.fatcerberus commentedon Jan 31, 2024
Given how often people want to be able to write
"foo" | "bar" | string
and have that mean something, I feel like there's almost certainly code in the wild that does stuff likewhich would likely be broken by
type
no longer being considered a discriminant. And we don't have negated types yet, so...ahejlsberg commentedon Jan 31, 2024
There probably is, but it is hard to see what it accomplishes. The only property that can be made accessible through narrowing is
catchall
and only when narrowed to atype
that isn't"foo"
or"bar"
.