-
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
String constant fails to infer type inside nested unions #54498
Comments
Not related to the issue, but the types being defined in reverse order in the repro is kind of confusing. I kept going "Wait, where is this type coming from?" as I read through it. At one point I even suspected some of the types might be external. |
My apologies. That was not coincidental but at the same time not intentional. I produced this example from a real world code that was faulting, and the way I did it was to copy and paste in first the failing lines, and then recursively grab any undefined types (and paste them below the existing lines, whoops), until I had it all. I'll update the example to flip everything around. |
The change between origin/release-5.0 and origin/release-5.1 occurred at 4fcb8b8. |
Bisects to #51884, FYI @jakebailey @nebkat @weswigham in case you have any immediate takes |
This appears to be a very obscure unintended consequence of #43633/#43937 within BackgroundWhen discriminating between union constituents we consider not just the existence/value of the discriminator property but also the omission of properties that are required/optional in some constituents. This is implemented by checking which of the constituent types allow optional values for discriminable properties which are not present in the object literal - that is whether Discriminable properties are known properties which have differing types or differing literal values amongst some of but not necessarily all of the union constituents. Index signatures do not generate any discriminable properties, but they don't prevent them from being made between other constituents either, the property just gets marked as "partial". function discriminateContextualTypeByObjectMembers(node: ObjectLiteralExpression, contextualType: UnionType) {
return getMatchingUnionConstituentForObjectLiteral(contextualType, node) || discriminateTypeByDiscriminableItems(contextualType,
concatenate(
map(
filter(node.properties, p => !!p.symbol && p.kind === SyntaxKind.PropertyAssignment && isPossiblyDiscriminantValue(p.initializer) && isDiscriminantProperty(contextualType, p.symbol.escapedName)),
prop => ([() => getContextFreeTypeOfExpression((prop as PropertyAssignment).initializer), prop.symbol.escapedName] as [() => Type, __String])
),
===> map(
===> filter(getPropertiesOfType(contextualType), s => !!(s.flags & SymbolFlags.Optional) && !!node?.symbol?.members && !node.symbol.members.has(s.escapedName) && isDiscriminantProperty(contextualType, s.escapedName)),
===> s => [() => undefinedType, s.escapedName] as [() => Type, __String]
===> )
),
isTypeAssignableTo
);
} Side note it might be good if some comments got added here to explain that this is doing property omission discrimination as it is not immediately obvious. In the original example for #41759 this makes sense as the property is explicitly required in some union constituents but not others i.e. we can discriminate on the omission of type DiscriminatorTrue = {
disc: true;
cb: (x: string) => void;
}
type DiscriminatorFalse = {
disc?: false;
cb: (x: number) => void;
} IssueHowever we now take a problematic example similar to the original post: export type FooA = { foo?: 'a' }
export type FooB = { foo?: 'b' }
export type IndexSignatureType = {
[key: string]: { bar: 'baz' };
}
export type Problem = IndexSignatureType | FooA | FooB;
const test: Problem = {
1: {
bar: 'baz'
}
};
Interestingly the problem can be replicated before #51884 by making export type FooA = { foo: 'a' }
export type FooB = { foo?: 'b' }
export type IndexSignatureType = {
[key: string]: { bar: 'baz' };
}
export type Problem = IndexSignatureType | FooA | FooB;
const test: Problem = {
1: {
bar: 'baz' // Fails on 5.0.4: Type 'string' is not assignable to type '"baz"'. (2322)
}
}; Here SolutionI think the solution is either to skip omission based discrimination on "partial" properties altogether, or perhaps better yet to treat the index signature type as being optional and therefore passing the omission test. Here is a diff for the former solution, which fixes the original issue without failing any tests. diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 72d7f692f9..fa281c99b0 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -25491,7 +25491,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return false;
}
- function isDiscriminantProperty(type: Type | undefined, name: __String) {
+ function isDiscriminantProperty(type: Type | undefined, name: __String, skipPartial: boolean = false) {
if (type && type.flags & TypeFlags.Union) {
const prop = getUnionOrIntersectionProperty(type as UnionType, name);
if (prop && getCheckFlags(prop) & CheckFlags.SyntheticProperty) {
@@ -25501,7 +25501,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
((prop as TransientSymbol).links.checkFlags & CheckFlags.Discriminant) === CheckFlags.Discriminant &&
!isGenericType(getTypeOfSymbol(prop));
}
- return !!(prop as TransientSymbol).links.isDiscriminantProperty;
+ return !!(prop as TransientSymbol).links.isDiscriminantProperty && (!skipPartial || !((prop as TransientSymbol).links.checkFlags & CheckFlags.Partial));
}
}
return false;
@@ -29358,7 +29358,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
prop => ([() => getContextFreeTypeOfExpression((prop as PropertyAssignment).initializer), prop.symbol.escapedName] as [() => Type, __String])
),
map(
- filter(getPropertiesOfType(contextualType), s => !!(s.flags & SymbolFlags.Optional) && !!node?.symbol?.members && !node.symbol.members.has(s.escapedName) && isDiscriminantProperty(contextualType, s.escapedName)),
+ filter(getPropertiesOfType(contextualType), s => !!(s.flags & SymbolFlags.Optional) && !!node?.symbol?.members && !node.symbol.members.has(s.escapedName) && isDiscriminantProperty(contextualType, s.escapedName, true)),
s => [() => undefinedType, s.escapedName] as [() => Type, __String]
)
), I will try to submit a PR later this week but I may be busy, feel free to proceed with the above if it is the right approach. Hope this helps! |
Bug Report
π Search Terms
infer contextual string union
π Version & Regression Information
β― Playground Link
Playground link with relevant code
π» Code
π Actual behavior
The declaration of
withoutAsConst
fails, astype: "text"
fails to be inferred as"text"
, and instead infers asstring
in this situation:π Expected behavior
The declaration of
withoutAsConst
should succeed. Typescript understands that the only valid values in this position are"text" | "image" | "video" | "document" | undefined
from the contextual type, so the literal"text"
should match that.The text was updated successfully, but these errors were encountered: