From 771a0559d008aec64d010a058d006199610ad676 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 7 Mar 2021 16:05:39 -0800 Subject: [PATCH 1/2] Fix narrowing of intersections of type variables and primitive types --- src/compiler/checker.ts | 46 +++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e5756b54b6ac9..aa4f4e68861d1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -17947,8 +17947,21 @@ namespace ts { if (target.flags & TypeFlags.Intersection) { return typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors, IntersectionState.Target); } - // Source is an intersection. Check to see if any constituents of the intersection are immediately related - // to the target. + // Source is an intersection. For the comparable relation, if the target is a primitive type we hoist the + // constraints of all non-primitive types in the source into a new intersection. We do this because the + // intersection may further constrain the constraints of the non-primitive types. For example, given a type + // parameter 'T extends 1 | 2', the intersection 'T & 1' should be reduced to '1' such that it doesn't + // appear to be comparable to '2'. + if (relation === comparableRelation && target.flags & TypeFlags.Primitive) { + const constraints = sameMap((source).types, t => t.flags & TypeFlags.Primitive ? t : getBaseConstraintOfType(t) || unknownType); + if (constraints !== (source).types) { + source = getIntersectionType(constraints); + if (!(source.flags & TypeFlags.Intersection)) { + return isRelatedTo(source, target, /*reportErrors*/ false); + } + } + } + // Check to see if any constituents of the intersection are immediately related to the target. // // Don't report errors though. Checking whether a constituent is related to the source is not actually // useful and leads to some confusing error messages. Instead it is better to let the below checks @@ -19680,6 +19693,15 @@ namespace ts { return !!(type.flags & TypeFlags.Unit); } + function isUnitLikeType(type: Type): boolean { + return type.flags & TypeFlags.Intersection ? some((type).types, isUnitType) : + !!(type.flags & TypeFlags.Unit); + } + + function extractUnitType(type: Type) { + return type.flags & TypeFlags.Intersection ? find((type).types, isUnitType) || type : type; + } + function isLiteralType(type: Type): boolean { return type.flags & TypeFlags.Boolean ? true : type.flags & TypeFlags.Union ? type.flags & TypeFlags.EnumLiteral ? true : every((type).types, isUnitType) : @@ -21663,14 +21685,6 @@ namespace ts { return declaredType; } - function getTypeFactsOfTypes(types: Type[]): TypeFacts { - let result: TypeFacts = TypeFacts.None; - for (const t of types) { - result |= getTypeFacts(t); - } - return result; - } - function isFunctionObjectType(type: ObjectType): boolean { // We do a quick check for a "bind" property before performing the more expensive subtype // check. This gives us a quicker out in the common case where an object type is not a function. @@ -21742,8 +21756,11 @@ namespace ts { return !isPatternLiteralType(type) ? getTypeFacts(getBaseConstraintOfType(type) || unknownType) : strictNullChecks ? TypeFacts.NonEmptyStringStrictFacts : TypeFacts.NonEmptyStringFacts; } - if (flags & TypeFlags.UnionOrIntersection) { - return getTypeFactsOfTypes((type).types); + if (flags & TypeFlags.Union) { + return reduceLeft((type).types, (facts, t) => facts | getTypeFacts(t), TypeFacts.None); + } + if (flags & TypeFlags.Intersection) { + return reduceLeft((type).types, (facts, t) => facts & getTypeFacts(t), TypeFacts.All); } return TypeFacts.All; } @@ -23075,8 +23092,7 @@ namespace ts { return replacePrimitivesWithLiterals(filterType(type, filterFn), valueType); } if (isUnitType(valueType)) { - const regularType = getRegularTypeOfLiteralType(valueType); - return filterType(type, t => isUnitType(t) ? !areTypesComparable(t, valueType) : getRegularTypeOfLiteralType(t) !== regularType); + return filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType))); } return type; } @@ -23158,7 +23174,7 @@ namespace ts { if (!hasDefaultClause) { return caseType; } - const defaultType = filterType(type, t => !(isUnitType(t) && contains(switchTypes, getRegularTypeOfLiteralType(t)))); + const defaultType = filterType(type, t => !(isUnitLikeType(t) && contains(switchTypes, getRegularTypeOfLiteralType(extractUnitType(t))))); return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); } From 5a8a0bbfe7e9fb69dfd0a419060de58383036984 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 7 Mar 2021 16:08:53 -0800 Subject: [PATCH 2/2] Add tests --- .../intersectionNarrowing.errors.txt | 44 +++++++++ .../reference/intersectionNarrowing.js | 78 ++++++++++++++++ .../reference/intersectionNarrowing.symbols | 89 +++++++++++++++++++ .../reference/intersectionNarrowing.types | 83 +++++++++++++++++ .../intersection/intersectionNarrowing.ts | 39 ++++++++ 5 files changed, 333 insertions(+) create mode 100644 tests/baselines/reference/intersectionNarrowing.errors.txt create mode 100644 tests/baselines/reference/intersectionNarrowing.js create mode 100644 tests/baselines/reference/intersectionNarrowing.symbols create mode 100644 tests/baselines/reference/intersectionNarrowing.types create mode 100644 tests/cases/conformance/types/intersection/intersectionNarrowing.ts diff --git a/tests/baselines/reference/intersectionNarrowing.errors.txt b/tests/baselines/reference/intersectionNarrowing.errors.txt new file mode 100644 index 0000000000000..d86340ca4e695 --- /dev/null +++ b/tests/baselines/reference/intersectionNarrowing.errors.txt @@ -0,0 +1,44 @@ +tests/cases/conformance/types/intersection/intersectionNarrowing.ts(36,16): error TS2367: This condition will always return 'false' since the types 'T & number' and 'string' have no overlap. + + +==== tests/cases/conformance/types/intersection/intersectionNarrowing.ts (1 errors) ==== + // Repros from #43130 + + function f1(x: T & string | T & undefined) { + if (x) { + x; // Should narrow to T & string + } + } + + function f2(x: T & string | T & undefined) { + if (x !== undefined) { + x; // Should narrow to T & string + } + else { + x; // Should narrow to T & undefined + } + } + + function f3(x: T & string | T & number) { + if (typeof x === "string") { + x; // Should narrow to T & string + } + else { + x; // Should narrow to T & number + } + } + + function f4(x: T & 1 | T & 2) { + switch (x) { + case 1: x; break; // T & 1 + case 2: x; break; // T & 2 + default: x; // Should narrow to never + } + } + + function f5(x: T & number) { + const t1 = x === "hello"; // Should be an error + ~~~~~~~~~~~~~ +!!! error TS2367: This condition will always return 'false' since the types 'T & number' and 'string' have no overlap. + } + \ No newline at end of file diff --git a/tests/baselines/reference/intersectionNarrowing.js b/tests/baselines/reference/intersectionNarrowing.js new file mode 100644 index 0000000000000..b5e47dfd60d9a --- /dev/null +++ b/tests/baselines/reference/intersectionNarrowing.js @@ -0,0 +1,78 @@ +//// [intersectionNarrowing.ts] +// Repros from #43130 + +function f1(x: T & string | T & undefined) { + if (x) { + x; // Should narrow to T & string + } +} + +function f2(x: T & string | T & undefined) { + if (x !== undefined) { + x; // Should narrow to T & string + } + else { + x; // Should narrow to T & undefined + } +} + +function f3(x: T & string | T & number) { + if (typeof x === "string") { + x; // Should narrow to T & string + } + else { + x; // Should narrow to T & number + } +} + +function f4(x: T & 1 | T & 2) { + switch (x) { + case 1: x; break; // T & 1 + case 2: x; break; // T & 2 + default: x; // Should narrow to never + } +} + +function f5(x: T & number) { + const t1 = x === "hello"; // Should be an error +} + + +//// [intersectionNarrowing.js] +"use strict"; +// Repros from #43130 +function f1(x) { + if (x) { + x; // Should narrow to T & string + } +} +function f2(x) { + if (x !== undefined) { + x; // Should narrow to T & string + } + else { + x; // Should narrow to T & undefined + } +} +function f3(x) { + if (typeof x === "string") { + x; // Should narrow to T & string + } + else { + x; // Should narrow to T & number + } +} +function f4(x) { + switch (x) { + case 1: + x; + break; // T & 1 + case 2: + x; + break; // T & 2 + default: x; // Should narrow to never + } +} +function f5(x) { + var t1 = x === "hello"; // Should be an error +} diff --git a/tests/baselines/reference/intersectionNarrowing.symbols b/tests/baselines/reference/intersectionNarrowing.symbols new file mode 100644 index 0000000000000..f8dc73840a4d8 --- /dev/null +++ b/tests/baselines/reference/intersectionNarrowing.symbols @@ -0,0 +1,89 @@ +=== tests/cases/conformance/types/intersection/intersectionNarrowing.ts === +// Repros from #43130 + +function f1(x: T & string | T & undefined) { +>f1 : Symbol(f1, Decl(intersectionNarrowing.ts, 0, 0)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 2, 12)) +>x : Symbol(x, Decl(intersectionNarrowing.ts, 2, 15)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 2, 12)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 2, 12)) + + if (x) { +>x : Symbol(x, Decl(intersectionNarrowing.ts, 2, 15)) + + x; // Should narrow to T & string +>x : Symbol(x, Decl(intersectionNarrowing.ts, 2, 15)) + } +} + +function f2(x: T & string | T & undefined) { +>f2 : Symbol(f2, Decl(intersectionNarrowing.ts, 6, 1)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 8, 12)) +>x : Symbol(x, Decl(intersectionNarrowing.ts, 8, 15)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 8, 12)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 8, 12)) + + if (x !== undefined) { +>x : Symbol(x, Decl(intersectionNarrowing.ts, 8, 15)) +>undefined : Symbol(undefined) + + x; // Should narrow to T & string +>x : Symbol(x, Decl(intersectionNarrowing.ts, 8, 15)) + } + else { + x; // Should narrow to T & undefined +>x : Symbol(x, Decl(intersectionNarrowing.ts, 8, 15)) + } +} + +function f3(x: T & string | T & number) { +>f3 : Symbol(f3, Decl(intersectionNarrowing.ts, 15, 1)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 17, 12)) +>x : Symbol(x, Decl(intersectionNarrowing.ts, 17, 15)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 17, 12)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 17, 12)) + + if (typeof x === "string") { +>x : Symbol(x, Decl(intersectionNarrowing.ts, 17, 15)) + + x; // Should narrow to T & string +>x : Symbol(x, Decl(intersectionNarrowing.ts, 17, 15)) + } + else { + x; // Should narrow to T & number +>x : Symbol(x, Decl(intersectionNarrowing.ts, 17, 15)) + } +} + +function f4(x: T & 1 | T & 2) { +>f4 : Symbol(f4, Decl(intersectionNarrowing.ts, 24, 1)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 26, 12)) +>x : Symbol(x, Decl(intersectionNarrowing.ts, 26, 15)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 26, 12)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 26, 12)) + + switch (x) { +>x : Symbol(x, Decl(intersectionNarrowing.ts, 26, 15)) + + case 1: x; break; // T & 1 +>x : Symbol(x, Decl(intersectionNarrowing.ts, 26, 15)) + + case 2: x; break; // T & 2 +>x : Symbol(x, Decl(intersectionNarrowing.ts, 26, 15)) + + default: x; // Should narrow to never +>x : Symbol(x, Decl(intersectionNarrowing.ts, 26, 15)) + } +} + +function f5(x: T & number) { +>f5 : Symbol(f5, Decl(intersectionNarrowing.ts, 32, 1)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 34, 12)) +>x : Symbol(x, Decl(intersectionNarrowing.ts, 34, 39)) +>T : Symbol(T, Decl(intersectionNarrowing.ts, 34, 12)) + + const t1 = x === "hello"; // Should be an error +>t1 : Symbol(t1, Decl(intersectionNarrowing.ts, 35, 9)) +>x : Symbol(x, Decl(intersectionNarrowing.ts, 34, 39)) +} + diff --git a/tests/baselines/reference/intersectionNarrowing.types b/tests/baselines/reference/intersectionNarrowing.types new file mode 100644 index 0000000000000..049e6c976eb6d --- /dev/null +++ b/tests/baselines/reference/intersectionNarrowing.types @@ -0,0 +1,83 @@ +=== tests/cases/conformance/types/intersection/intersectionNarrowing.ts === +// Repros from #43130 + +function f1(x: T & string | T & undefined) { +>f1 : (x: (T & string) | (T & undefined)) => void +>x : (T & string) | (T & undefined) + + if (x) { +>x : (T & string) | (T & undefined) + + x; // Should narrow to T & string +>x : T & string + } +} + +function f2(x: T & string | T & undefined) { +>f2 : (x: (T & string) | (T & undefined)) => void +>x : (T & string) | (T & undefined) + + if (x !== undefined) { +>x !== undefined : boolean +>x : (T & string) | (T & undefined) +>undefined : undefined + + x; // Should narrow to T & string +>x : T & string + } + else { + x; // Should narrow to T & undefined +>x : T & undefined + } +} + +function f3(x: T & string | T & number) { +>f3 : (x: (T & string) | (T & number)) => void +>x : (T & string) | (T & number) + + if (typeof x === "string") { +>typeof x === "string" : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : (T & string) | (T & number) +>"string" : "string" + + x; // Should narrow to T & string +>x : T & string + } + else { + x; // Should narrow to T & number +>x : T & number + } +} + +function f4(x: T & 1 | T & 2) { +>f4 : (x: (T & 1) | (T & 2)) => void +>x : (T & 1) | (T & 2) + + switch (x) { +>x : (T & 1) | (T & 2) + + case 1: x; break; // T & 1 +>1 : 1 +>x : T & 1 + + case 2: x; break; // T & 2 +>2 : 2 +>x : T & 2 + + default: x; // Should narrow to never +>x : never + } +} + +function f5(x: T & number) { +>f5 : (x: T & number) => void +>x : T & number + + const t1 = x === "hello"; // Should be an error +>t1 : boolean +>x === "hello" : boolean +>x : T & number +>"hello" : "hello" +} + diff --git a/tests/cases/conformance/types/intersection/intersectionNarrowing.ts b/tests/cases/conformance/types/intersection/intersectionNarrowing.ts new file mode 100644 index 0000000000000..32fd6d6cfcf0d --- /dev/null +++ b/tests/cases/conformance/types/intersection/intersectionNarrowing.ts @@ -0,0 +1,39 @@ +// @strict: true + +// Repros from #43130 + +function f1(x: T & string | T & undefined) { + if (x) { + x; // Should narrow to T & string + } +} + +function f2(x: T & string | T & undefined) { + if (x !== undefined) { + x; // Should narrow to T & string + } + else { + x; // Should narrow to T & undefined + } +} + +function f3(x: T & string | T & number) { + if (typeof x === "string") { + x; // Should narrow to T & string + } + else { + x; // Should narrow to T & number + } +} + +function f4(x: T & 1 | T & 2) { + switch (x) { + case 1: x; break; // T & 1 + case 2: x; break; // T & 2 + default: x; // Should narrow to never + } +} + +function f5(x: T & number) { + const t1 = x === "hello"; // Should be an error +}