From 2f0c6070cb856588eea550932375c1c7ac183a69 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 26 Sep 2021 14:13:42 -0700 Subject: [PATCH] Properly handle missingType in intersections (#46052) * Properly handle missingType in intersections * Add regression tests * Accept new baselines * Fix tests --- src/compiler/checker.ts | 45 ++++++++++++------- src/compiler/types.ts | 2 +- .../strictOptionalProperties1.errors.txt | 23 ++++++++++ .../reference/strictOptionalProperties1.js | 38 ++++++++++++++++ .../strictOptionalProperties1.symbols | 43 ++++++++++++++++++ .../reference/strictOptionalProperties1.types | 32 +++++++++++++ .../compiler/strictOptionalProperties1.ts | 23 ++++++++++ 7 files changed, 188 insertions(+), 18 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 20078bc12d8..074dd90fe6d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -14030,7 +14030,6 @@ namespace ts { // We ignore 'never' types in unions if (!(flags & TypeFlags.Never)) { includes |= flags & TypeFlags.IncludesMask; - if (flags & TypeFlags.StructuredOrInstantiable) includes |= TypeFlags.IncludesStructuredOrInstantiable; if (type === wildcardType) includes |= TypeFlags.IncludesWildcard; if (!strictNullChecks && flags & TypeFlags.Nullable) { if (!(getObjectFlags(type) & ObjectFlags.ContainsWideningType)) includes |= TypeFlags.IncludesNonWideningType; @@ -14337,13 +14336,19 @@ namespace ts { if (flags & TypeFlags.AnyOrUnknown) { if (type === wildcardType) includes |= TypeFlags.IncludesWildcard; } - else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !typeSet.has(type.id.toString())) { - if (type.flags & TypeFlags.Unit && includes & TypeFlags.Unit) { - // We have seen two distinct unit types which means we should reduce to an - // empty intersection. Adding TypeFlags.NonPrimitive causes that to happen. - includes |= TypeFlags.NonPrimitive; + else if (strictNullChecks || !(flags & TypeFlags.Nullable)) { + if (exactOptionalPropertyTypes && type === missingType) { + includes |= TypeFlags.IncludesMissingType; + type = undefinedType; + } + if (!typeSet.has(type.id.toString())) { + if (type.flags & TypeFlags.Unit && includes & TypeFlags.Unit) { + // We have seen two distinct unit types which means we should reduce to an + // empty intersection. Adding TypeFlags.NonPrimitive causes that to happen. + includes |= TypeFlags.NonPrimitive; + } + typeSet.set(type.id.toString(), type); } - typeSet.set(type.id.toString(), type); } includes |= flags & TypeFlags.IncludesMask; } @@ -14418,14 +14423,14 @@ namespace ts { return false; } - function extractIrreducible(types: Type[], flag: TypeFlags) { - if (every(types, t => !!(t.flags & TypeFlags.Union) && some((t as UnionType).types, tt => !!(tt.flags & flag)))) { - for (let i = 0; i < types.length; i++) { - types[i] = filterType(types[i], t => !(t.flags & flag)); - } - return true; + function eachIsUnionContaining(types: Type[], flag: TypeFlags) { + return every(types, t => !!(t.flags & TypeFlags.Union) && some((t as UnionType).types, tt => !!(tt.flags & flag))); + } + + function removeFromEach(types: Type[], flag: TypeFlags) { + for (let i = 0; i < types.length; i++) { + types[i] = filterType(types[i], t => !(t.flags & flag)); } - return false; } // If the given list of types contains more than one union of primitive types, replace the @@ -14535,6 +14540,9 @@ namespace ts { if (includes & TypeFlags.IncludesEmptyObject && includes & TypeFlags.Object) { orderedRemoveItemAt(typeSet, findIndex(typeSet, isEmptyAnonymousObjectType)); } + if (includes & TypeFlags.IncludesMissingType) { + typeSet[typeSet.indexOf(undefinedType)] = missingType; + } if (typeSet.length === 0) { return unknownType; } @@ -14551,10 +14559,13 @@ namespace ts { // reduced we'll never reduce again, so this occurs at most once. result = getIntersectionType(typeSet, aliasSymbol, aliasTypeArguments); } - else if (extractIrreducible(typeSet, TypeFlags.Undefined)) { - result = getUnionType([getIntersectionType(typeSet), undefinedType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments); + else if (eachIsUnionContaining(typeSet, TypeFlags.Undefined)) { + const undefinedOrMissingType = exactOptionalPropertyTypes && some(typeSet, t => containsType((t as UnionType).types, missingType)) ? missingType : undefinedType; + removeFromEach(typeSet, TypeFlags.Undefined); + result = getUnionType([getIntersectionType(typeSet), undefinedOrMissingType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments); } - else if (extractIrreducible(typeSet, TypeFlags.Null)) { + else if (eachIsUnionContaining(typeSet, TypeFlags.Null)) { + removeFromEach(typeSet, TypeFlags.Null); result = getUnionType([getIntersectionType(typeSet), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments); } else { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index c3bb8de6344..d048f6897bd 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5181,7 +5181,7 @@ namespace ts { IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive | TemplateLiteral, // The following flags are used for different purposes during union and intersection type construction /* @internal */ - IncludesStructuredOrInstantiable = TypeParameter, + IncludesMissingType = TypeParameter, /* @internal */ IncludesNonWideningType = Index, /* @internal */ diff --git a/tests/baselines/reference/strictOptionalProperties1.errors.txt b/tests/baselines/reference/strictOptionalProperties1.errors.txt index 68c7da1f9e4..dc2b031a307 100644 --- a/tests/baselines/reference/strictOptionalProperties1.errors.txt +++ b/tests/baselines/reference/strictOptionalProperties1.errors.txt @@ -329,4 +329,27 @@ tests/cases/compiler/strictOptionalProperties1.ts(211,1): error TS2322: Type 'st ~ !!! error TS2322: Type 'string | boolean | undefined' is not assignable to type '{ [x: string]: string | number; }'. !!! error TS2322: Type 'undefined' is not assignable to type '{ [x: string]: string | number; }'. + + // Repro from #46004 + + interface PropsFromReact { + onClick?: () => void; + } + + interface PropsFromMaterialUI { + onClick?: (() => void) | undefined; + } + + type TheTypeFromMaterialUI = PropsFromReact & PropsFromMaterialUI; + + interface NavBottomListItem extends TheTypeFromMaterialUI { + value: string; + } + + // Repro from #46004 + + type UA = undefined; // Explicit undefined type + type UB = { x?: never }['x']; // undefined from missing property + + type UC = UA & UB; // undefined \ No newline at end of file diff --git a/tests/baselines/reference/strictOptionalProperties1.js b/tests/baselines/reference/strictOptionalProperties1.js index ba3536e97f7..faef4708b04 100644 --- a/tests/baselines/reference/strictOptionalProperties1.js +++ b/tests/baselines/reference/strictOptionalProperties1.js @@ -210,6 +210,29 @@ a = b; a = c; a = d; // Error a = e; // Error + +// Repro from #46004 + +interface PropsFromReact { + onClick?: () => void; +} + +interface PropsFromMaterialUI { + onClick?: (() => void) | undefined; +} + +type TheTypeFromMaterialUI = PropsFromReact & PropsFromMaterialUI; + +interface NavBottomListItem extends TheTypeFromMaterialUI { + value: string; +} + +// Repro from #46004 + +type UA = undefined; // Explicit undefined type +type UB = { x?: never }['x']; // undefined from missing property + +type UC = UA & UB; // undefined //// [strictOptionalProperties1.js] @@ -453,3 +476,18 @@ declare var e: { a: number; b?: string | undefined; }; +interface PropsFromReact { + onClick?: () => void; +} +interface PropsFromMaterialUI { + onClick?: (() => void) | undefined; +} +declare type TheTypeFromMaterialUI = PropsFromReact & PropsFromMaterialUI; +interface NavBottomListItem extends TheTypeFromMaterialUI { + value: string; +} +declare type UA = undefined; +declare type UB = { + x?: never; +}['x']; +declare type UC = UA & UB; diff --git a/tests/baselines/reference/strictOptionalProperties1.symbols b/tests/baselines/reference/strictOptionalProperties1.symbols index b3cd1b5f15a..66550fd3e3b 100644 --- a/tests/baselines/reference/strictOptionalProperties1.symbols +++ b/tests/baselines/reference/strictOptionalProperties1.symbols @@ -670,3 +670,46 @@ a = e; // Error >a : Symbol(a, Decl(strictOptionalProperties1.ts, 201, 11)) >e : Symbol(e, Decl(strictOptionalProperties1.ts, 189, 13)) +// Repro from #46004 + +interface PropsFromReact { +>PropsFromReact : Symbol(PropsFromReact, Decl(strictOptionalProperties1.ts, 210, 6)) + + onClick?: () => void; +>onClick : Symbol(PropsFromReact.onClick, Decl(strictOptionalProperties1.ts, 214, 26)) +} + +interface PropsFromMaterialUI { +>PropsFromMaterialUI : Symbol(PropsFromMaterialUI, Decl(strictOptionalProperties1.ts, 216, 1)) + + onClick?: (() => void) | undefined; +>onClick : Symbol(PropsFromMaterialUI.onClick, Decl(strictOptionalProperties1.ts, 218, 31)) +} + +type TheTypeFromMaterialUI = PropsFromReact & PropsFromMaterialUI; +>TheTypeFromMaterialUI : Symbol(TheTypeFromMaterialUI, Decl(strictOptionalProperties1.ts, 220, 1)) +>PropsFromReact : Symbol(PropsFromReact, Decl(strictOptionalProperties1.ts, 210, 6)) +>PropsFromMaterialUI : Symbol(PropsFromMaterialUI, Decl(strictOptionalProperties1.ts, 216, 1)) + +interface NavBottomListItem extends TheTypeFromMaterialUI { +>NavBottomListItem : Symbol(NavBottomListItem, Decl(strictOptionalProperties1.ts, 222, 66)) +>TheTypeFromMaterialUI : Symbol(TheTypeFromMaterialUI, Decl(strictOptionalProperties1.ts, 220, 1)) + + value: string; +>value : Symbol(NavBottomListItem.value, Decl(strictOptionalProperties1.ts, 224, 59)) +} + +// Repro from #46004 + +type UA = undefined; // Explicit undefined type +>UA : Symbol(UA, Decl(strictOptionalProperties1.ts, 226, 1)) + +type UB = { x?: never }['x']; // undefined from missing property +>UB : Symbol(UB, Decl(strictOptionalProperties1.ts, 230, 20)) +>x : Symbol(x, Decl(strictOptionalProperties1.ts, 231, 11)) + +type UC = UA & UB; // undefined +>UC : Symbol(UC, Decl(strictOptionalProperties1.ts, 231, 29)) +>UA : Symbol(UA, Decl(strictOptionalProperties1.ts, 226, 1)) +>UB : Symbol(UB, Decl(strictOptionalProperties1.ts, 230, 20)) + diff --git a/tests/baselines/reference/strictOptionalProperties1.types b/tests/baselines/reference/strictOptionalProperties1.types index 4eb23fd31c8..86e2e6e5c12 100644 --- a/tests/baselines/reference/strictOptionalProperties1.types +++ b/tests/baselines/reference/strictOptionalProperties1.types @@ -780,3 +780,35 @@ a = e; // Error >a : { [x: string]: string | number; } >e : string | boolean | undefined +// Repro from #46004 + +interface PropsFromReact { + onClick?: () => void; +>onClick : (() => void) | undefined +} + +interface PropsFromMaterialUI { + onClick?: (() => void) | undefined; +>onClick : (() => void) | undefined +} + +type TheTypeFromMaterialUI = PropsFromReact & PropsFromMaterialUI; +>TheTypeFromMaterialUI : TheTypeFromMaterialUI + +interface NavBottomListItem extends TheTypeFromMaterialUI { + value: string; +>value : string +} + +// Repro from #46004 + +type UA = undefined; // Explicit undefined type +>UA : undefined + +type UB = { x?: never }['x']; // undefined from missing property +>UB : undefined +>x : undefined + +type UC = UA & UB; // undefined +>UC : undefined + diff --git a/tests/cases/compiler/strictOptionalProperties1.ts b/tests/cases/compiler/strictOptionalProperties1.ts index 2babf2bdb5b..86f38f2a598 100644 --- a/tests/cases/compiler/strictOptionalProperties1.ts +++ b/tests/cases/compiler/strictOptionalProperties1.ts @@ -213,3 +213,26 @@ a = b; a = c; a = d; // Error a = e; // Error + +// Repro from #46004 + +interface PropsFromReact { + onClick?: () => void; +} + +interface PropsFromMaterialUI { + onClick?: (() => void) | undefined; +} + +type TheTypeFromMaterialUI = PropsFromReact & PropsFromMaterialUI; + +interface NavBottomListItem extends TheTypeFromMaterialUI { + value: string; +} + +// Repro from #46004 + +type UA = undefined; // Explicit undefined type +type UB = { x?: never }['x']; // undefined from missing property + +type UC = UA & UB; // undefined