Skip to content

Commit a21f61f

Browse files
authored
Fix narrowing of intersections of type variables and primitive types (#43131)
* Fix narrowing of intersections of type variables and primitive types * Add tests
1 parent ec77bff commit a21f61f

File tree

6 files changed

+364
-15
lines changed

6 files changed

+364
-15
lines changed

src/compiler/checker.ts

+31-15
Original file line numberDiff line numberDiff line change
@@ -17975,8 +17975,21 @@ namespace ts {
1797517975
if (target.flags & TypeFlags.Intersection) {
1797617976
return typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors, IntersectionState.Target);
1797717977
}
17978-
// Source is an intersection. Check to see if any constituents of the intersection are immediately related
17979-
// to the target.
17978+
// Source is an intersection. For the comparable relation, if the target is a primitive type we hoist the
17979+
// constraints of all non-primitive types in the source into a new intersection. We do this because the
17980+
// intersection may further constrain the constraints of the non-primitive types. For example, given a type
17981+
// parameter 'T extends 1 | 2', the intersection 'T & 1' should be reduced to '1' such that it doesn't
17982+
// appear to be comparable to '2'.
17983+
if (relation === comparableRelation && target.flags & TypeFlags.Primitive) {
17984+
const constraints = sameMap((<IntersectionType>source).types, t => t.flags & TypeFlags.Primitive ? t : getBaseConstraintOfType(t) || unknownType);
17985+
if (constraints !== (<IntersectionType>source).types) {
17986+
source = getIntersectionType(constraints);
17987+
if (!(source.flags & TypeFlags.Intersection)) {
17988+
return isRelatedTo(source, target, /*reportErrors*/ false);
17989+
}
17990+
}
17991+
}
17992+
// Check to see if any constituents of the intersection are immediately related to the target.
1798017993
//
1798117994
// Don't report errors though. Checking whether a constituent is related to the source is not actually
1798217995
// useful and leads to some confusing error messages. Instead it is better to let the below checks
@@ -19738,6 +19751,15 @@ namespace ts {
1973819751
return !!(type.flags & TypeFlags.Unit);
1973919752
}
1974019753

19754+
function isUnitLikeType(type: Type): boolean {
19755+
return type.flags & TypeFlags.Intersection ? some((<IntersectionType>type).types, isUnitType) :
19756+
!!(type.flags & TypeFlags.Unit);
19757+
}
19758+
19759+
function extractUnitType(type: Type) {
19760+
return type.flags & TypeFlags.Intersection ? find((<IntersectionType>type).types, isUnitType) || type : type;
19761+
}
19762+
1974119763
function isLiteralType(type: Type): boolean {
1974219764
return type.flags & TypeFlags.Boolean ? true :
1974319765
type.flags & TypeFlags.Union ? type.flags & TypeFlags.EnumLiteral ? true : every((<UnionType>type).types, isUnitType) :
@@ -21721,14 +21743,6 @@ namespace ts {
2172121743
return declaredType;
2172221744
}
2172321745

21724-
function getTypeFactsOfTypes(types: Type[]): TypeFacts {
21725-
let result: TypeFacts = TypeFacts.None;
21726-
for (const t of types) {
21727-
result |= getTypeFacts(t);
21728-
}
21729-
return result;
21730-
}
21731-
2173221746
function isFunctionObjectType(type: ObjectType): boolean {
2173321747
// We do a quick check for a "bind" property before performing the more expensive subtype
2173421748
// check. This gives us a quicker out in the common case where an object type is not a function.
@@ -21800,8 +21814,11 @@ namespace ts {
2180021814
return !isPatternLiteralType(type) ? getTypeFacts(getBaseConstraintOfType(type) || unknownType) :
2180121815
strictNullChecks ? TypeFacts.NonEmptyStringStrictFacts : TypeFacts.NonEmptyStringFacts;
2180221816
}
21803-
if (flags & TypeFlags.UnionOrIntersection) {
21804-
return getTypeFactsOfTypes((<UnionOrIntersectionType>type).types);
21817+
if (flags & TypeFlags.Union) {
21818+
return reduceLeft((<UnionType>type).types, (facts, t) => facts | getTypeFacts(t), TypeFacts.None);
21819+
}
21820+
if (flags & TypeFlags.Intersection) {
21821+
return reduceLeft((<UnionType>type).types, (facts, t) => facts & getTypeFacts(t), TypeFacts.All);
2180521822
}
2180621823
return TypeFacts.All;
2180721824
}
@@ -23134,8 +23151,7 @@ namespace ts {
2313423151
return replacePrimitivesWithLiterals(filterType(type, filterFn), valueType);
2313523152
}
2313623153
if (isUnitType(valueType)) {
23137-
const regularType = getRegularTypeOfLiteralType(valueType);
23138-
return filterType(type, t => isUnitType(t) ? !areTypesComparable(t, valueType) : getRegularTypeOfLiteralType(t) !== regularType);
23154+
return filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType)));
2313923155
}
2314023156
return type;
2314123157
}
@@ -23217,7 +23233,7 @@ namespace ts {
2321723233
if (!hasDefaultClause) {
2321823234
return caseType;
2321923235
}
23220-
const defaultType = filterType(type, t => !(isUnitType(t) && contains(switchTypes, getRegularTypeOfLiteralType(t))));
23236+
const defaultType = filterType(type, t => !(isUnitLikeType(t) && contains(switchTypes, getRegularTypeOfLiteralType(extractUnitType(t)))));
2322123237
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
2322223238
}
2322323239

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
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.
2+
3+
4+
==== tests/cases/conformance/types/intersection/intersectionNarrowing.ts (1 errors) ====
5+
// Repros from #43130
6+
7+
function f1<T>(x: T & string | T & undefined) {
8+
if (x) {
9+
x; // Should narrow to T & string
10+
}
11+
}
12+
13+
function f2<T>(x: T & string | T & undefined) {
14+
if (x !== undefined) {
15+
x; // Should narrow to T & string
16+
}
17+
else {
18+
x; // Should narrow to T & undefined
19+
}
20+
}
21+
22+
function f3<T>(x: T & string | T & number) {
23+
if (typeof x === "string") {
24+
x; // Should narrow to T & string
25+
}
26+
else {
27+
x; // Should narrow to T & number
28+
}
29+
}
30+
31+
function f4<T>(x: T & 1 | T & 2) {
32+
switch (x) {
33+
case 1: x; break; // T & 1
34+
case 2: x; break; // T & 2
35+
default: x; // Should narrow to never
36+
}
37+
}
38+
39+
function f5<T extends string | number>(x: T & number) {
40+
const t1 = x === "hello"; // Should be an error
41+
~~~~~~~~~~~~~
42+
!!! error TS2367: This condition will always return 'false' since the types 'T & number' and 'string' have no overlap.
43+
}
44+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//// [intersectionNarrowing.ts]
2+
// Repros from #43130
3+
4+
function f1<T>(x: T & string | T & undefined) {
5+
if (x) {
6+
x; // Should narrow to T & string
7+
}
8+
}
9+
10+
function f2<T>(x: T & string | T & undefined) {
11+
if (x !== undefined) {
12+
x; // Should narrow to T & string
13+
}
14+
else {
15+
x; // Should narrow to T & undefined
16+
}
17+
}
18+
19+
function f3<T>(x: T & string | T & number) {
20+
if (typeof x === "string") {
21+
x; // Should narrow to T & string
22+
}
23+
else {
24+
x; // Should narrow to T & number
25+
}
26+
}
27+
28+
function f4<T>(x: T & 1 | T & 2) {
29+
switch (x) {
30+
case 1: x; break; // T & 1
31+
case 2: x; break; // T & 2
32+
default: x; // Should narrow to never
33+
}
34+
}
35+
36+
function f5<T extends string | number>(x: T & number) {
37+
const t1 = x === "hello"; // Should be an error
38+
}
39+
40+
41+
//// [intersectionNarrowing.js]
42+
"use strict";
43+
// Repros from #43130
44+
function f1(x) {
45+
if (x) {
46+
x; // Should narrow to T & string
47+
}
48+
}
49+
function f2(x) {
50+
if (x !== undefined) {
51+
x; // Should narrow to T & string
52+
}
53+
else {
54+
x; // Should narrow to T & undefined
55+
}
56+
}
57+
function f3(x) {
58+
if (typeof x === "string") {
59+
x; // Should narrow to T & string
60+
}
61+
else {
62+
x; // Should narrow to T & number
63+
}
64+
}
65+
function f4(x) {
66+
switch (x) {
67+
case 1:
68+
x;
69+
break; // T & 1
70+
case 2:
71+
x;
72+
break; // T & 2
73+
default: x; // Should narrow to never
74+
}
75+
}
76+
function f5(x) {
77+
var t1 = x === "hello"; // Should be an error
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
=== tests/cases/conformance/types/intersection/intersectionNarrowing.ts ===
2+
// Repros from #43130
3+
4+
function f1<T>(x: T & string | T & undefined) {
5+
>f1 : Symbol(f1, Decl(intersectionNarrowing.ts, 0, 0))
6+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 2, 12))
7+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 2, 15))
8+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 2, 12))
9+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 2, 12))
10+
11+
if (x) {
12+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 2, 15))
13+
14+
x; // Should narrow to T & string
15+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 2, 15))
16+
}
17+
}
18+
19+
function f2<T>(x: T & string | T & undefined) {
20+
>f2 : Symbol(f2, Decl(intersectionNarrowing.ts, 6, 1))
21+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 8, 12))
22+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 8, 15))
23+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 8, 12))
24+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 8, 12))
25+
26+
if (x !== undefined) {
27+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 8, 15))
28+
>undefined : Symbol(undefined)
29+
30+
x; // Should narrow to T & string
31+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 8, 15))
32+
}
33+
else {
34+
x; // Should narrow to T & undefined
35+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 8, 15))
36+
}
37+
}
38+
39+
function f3<T>(x: T & string | T & number) {
40+
>f3 : Symbol(f3, Decl(intersectionNarrowing.ts, 15, 1))
41+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 17, 12))
42+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 17, 15))
43+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 17, 12))
44+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 17, 12))
45+
46+
if (typeof x === "string") {
47+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 17, 15))
48+
49+
x; // Should narrow to T & string
50+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 17, 15))
51+
}
52+
else {
53+
x; // Should narrow to T & number
54+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 17, 15))
55+
}
56+
}
57+
58+
function f4<T>(x: T & 1 | T & 2) {
59+
>f4 : Symbol(f4, Decl(intersectionNarrowing.ts, 24, 1))
60+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 26, 12))
61+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 26, 15))
62+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 26, 12))
63+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 26, 12))
64+
65+
switch (x) {
66+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 26, 15))
67+
68+
case 1: x; break; // T & 1
69+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 26, 15))
70+
71+
case 2: x; break; // T & 2
72+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 26, 15))
73+
74+
default: x; // Should narrow to never
75+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 26, 15))
76+
}
77+
}
78+
79+
function f5<T extends string | number>(x: T & number) {
80+
>f5 : Symbol(f5, Decl(intersectionNarrowing.ts, 32, 1))
81+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 34, 12))
82+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 34, 39))
83+
>T : Symbol(T, Decl(intersectionNarrowing.ts, 34, 12))
84+
85+
const t1 = x === "hello"; // Should be an error
86+
>t1 : Symbol(t1, Decl(intersectionNarrowing.ts, 35, 9))
87+
>x : Symbol(x, Decl(intersectionNarrowing.ts, 34, 39))
88+
}
89+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
=== tests/cases/conformance/types/intersection/intersectionNarrowing.ts ===
2+
// Repros from #43130
3+
4+
function f1<T>(x: T & string | T & undefined) {
5+
>f1 : <T>(x: (T & string) | (T & undefined)) => void
6+
>x : (T & string) | (T & undefined)
7+
8+
if (x) {
9+
>x : (T & string) | (T & undefined)
10+
11+
x; // Should narrow to T & string
12+
>x : T & string
13+
}
14+
}
15+
16+
function f2<T>(x: T & string | T & undefined) {
17+
>f2 : <T>(x: (T & string) | (T & undefined)) => void
18+
>x : (T & string) | (T & undefined)
19+
20+
if (x !== undefined) {
21+
>x !== undefined : boolean
22+
>x : (T & string) | (T & undefined)
23+
>undefined : undefined
24+
25+
x; // Should narrow to T & string
26+
>x : T & string
27+
}
28+
else {
29+
x; // Should narrow to T & undefined
30+
>x : T & undefined
31+
}
32+
}
33+
34+
function f3<T>(x: T & string | T & number) {
35+
>f3 : <T>(x: (T & string) | (T & number)) => void
36+
>x : (T & string) | (T & number)
37+
38+
if (typeof x === "string") {
39+
>typeof x === "string" : boolean
40+
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
41+
>x : (T & string) | (T & number)
42+
>"string" : "string"
43+
44+
x; // Should narrow to T & string
45+
>x : T & string
46+
}
47+
else {
48+
x; // Should narrow to T & number
49+
>x : T & number
50+
}
51+
}
52+
53+
function f4<T>(x: T & 1 | T & 2) {
54+
>f4 : <T>(x: (T & 1) | (T & 2)) => void
55+
>x : (T & 1) | (T & 2)
56+
57+
switch (x) {
58+
>x : (T & 1) | (T & 2)
59+
60+
case 1: x; break; // T & 1
61+
>1 : 1
62+
>x : T & 1
63+
64+
case 2: x; break; // T & 2
65+
>2 : 2
66+
>x : T & 2
67+
68+
default: x; // Should narrow to never
69+
>x : never
70+
}
71+
}
72+
73+
function f5<T extends string | number>(x: T & number) {
74+
>f5 : <T extends string | number>(x: T & number) => void
75+
>x : T & number
76+
77+
const t1 = x === "hello"; // Should be an error
78+
>t1 : boolean
79+
>x === "hello" : boolean
80+
>x : T & number
81+
>"hello" : "hello"
82+
}
83+

0 commit comments

Comments
 (0)