Skip to content

Commit

Permalink
Fix narrowing of intersections of type variables and primitive types (#…
Browse files Browse the repository at this point in the history
…43131)

* Fix narrowing of intersections of type variables and primitive types

* Add tests
  • Loading branch information
ahejlsberg authored Mar 18, 2021
1 parent ec77bff commit a21f61f
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 15 deletions.
46 changes: 31 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17975,8 +17975,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((<IntersectionType>source).types, t => t.flags & TypeFlags.Primitive ? t : getBaseConstraintOfType(t) || unknownType);
if (constraints !== (<IntersectionType>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
Expand Down Expand Up @@ -19738,6 +19751,15 @@ namespace ts {
return !!(type.flags & TypeFlags.Unit);
}

function isUnitLikeType(type: Type): boolean {
return type.flags & TypeFlags.Intersection ? some((<IntersectionType>type).types, isUnitType) :
!!(type.flags & TypeFlags.Unit);
}

function extractUnitType(type: Type) {
return type.flags & TypeFlags.Intersection ? find((<IntersectionType>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((<UnionType>type).types, isUnitType) :
Expand Down Expand Up @@ -21721,14 +21743,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.
Expand Down Expand Up @@ -21800,8 +21814,11 @@ namespace ts {
return !isPatternLiteralType(type) ? getTypeFacts(getBaseConstraintOfType(type) || unknownType) :
strictNullChecks ? TypeFacts.NonEmptyStringStrictFacts : TypeFacts.NonEmptyStringFacts;
}
if (flags & TypeFlags.UnionOrIntersection) {
return getTypeFactsOfTypes((<UnionOrIntersectionType>type).types);
if (flags & TypeFlags.Union) {
return reduceLeft((<UnionType>type).types, (facts, t) => facts | getTypeFacts(t), TypeFacts.None);
}
if (flags & TypeFlags.Intersection) {
return reduceLeft((<UnionType>type).types, (facts, t) => facts & getTypeFacts(t), TypeFacts.All);
}
return TypeFacts.All;
}
Expand Down Expand Up @@ -23134,8 +23151,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;
}
Expand Down Expand Up @@ -23217,7 +23233,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]);
}

Expand Down
44 changes: 44 additions & 0 deletions tests/baselines/reference/intersectionNarrowing.errors.txt
Original file line number Diff line number Diff line change
@@ -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<T>(x: T & string | T & undefined) {
if (x) {
x; // Should narrow to T & string
}
}

function f2<T>(x: T & string | T & undefined) {
if (x !== undefined) {
x; // Should narrow to T & string
}
else {
x; // Should narrow to T & undefined
}
}

function f3<T>(x: T & string | T & number) {
if (typeof x === "string") {
x; // Should narrow to T & string
}
else {
x; // Should narrow to T & number
}
}

function f4<T>(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<T extends string | number>(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.
}

78 changes: 78 additions & 0 deletions tests/baselines/reference/intersectionNarrowing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//// [intersectionNarrowing.ts]
// Repros from #43130

function f1<T>(x: T & string | T & undefined) {
if (x) {
x; // Should narrow to T & string
}
}

function f2<T>(x: T & string | T & undefined) {
if (x !== undefined) {
x; // Should narrow to T & string
}
else {
x; // Should narrow to T & undefined
}
}

function f3<T>(x: T & string | T & number) {
if (typeof x === "string") {
x; // Should narrow to T & string
}
else {
x; // Should narrow to T & number
}
}

function f4<T>(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<T extends string | number>(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
}
89 changes: 89 additions & 0 deletions tests/baselines/reference/intersectionNarrowing.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
=== tests/cases/conformance/types/intersection/intersectionNarrowing.ts ===
// Repros from #43130

function f1<T>(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<T>(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<T>(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<T>(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<T extends string | number>(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))
}

83 changes: 83 additions & 0 deletions tests/baselines/reference/intersectionNarrowing.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
=== tests/cases/conformance/types/intersection/intersectionNarrowing.ts ===
// Repros from #43130

function f1<T>(x: T & string | T & undefined) {
>f1 : <T>(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<T>(x: T & string | T & undefined) {
>f2 : <T>(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<T>(x: T & string | T & number) {
>f3 : <T>(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<T>(x: T & 1 | T & 2) {
>f4 : <T>(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<T extends string | number>(x: T & number) {
>f5 : <T extends string | number>(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"
}

Loading

0 comments on commit a21f61f

Please sign in to comment.