Skip to content

Commit 2d457fc

Browse files
committed
Remove check narrowing only certain types, add test showing issues with this
1 parent 713c0e0 commit 2d457fc

File tree

5 files changed

+259
-57
lines changed

5 files changed

+259
-57
lines changed

src/compiler/checker.ts

Lines changed: 54 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6941,74 +6941,71 @@ namespace ts {
69416941
// Get the narrowed type of a given symbol at a given location
69426942
function getNarrowedTypeOfSymbol(symbol: Symbol, node: Node) {
69436943
let type = getTypeOfSymbol(symbol);
6944-
// Only narrow when symbol is variable of type any or an object, union, or type parameter type
69456944
if (node && symbol.flags & SymbolFlags.Variable) {
6946-
if (isTypeAny(type) || type.flags & (TypeFlags.ObjectType | TypeFlags.Union | TypeFlags.TypeParameter)) {
6947-
const declaration = getDeclarationOfKind(symbol, SyntaxKind.VariableDeclaration);
6948-
const top = declaration && getDeclarationContainer(declaration);
6949-
const originalType = type;
6950-
const nodeStack: {node: Node, child: Node}[] = [];
6951-
loop: while (node.parent) {
6952-
const child = node;
6953-
node = node.parent;
6954-
switch (node.kind) {
6955-
case SyntaxKind.IfStatement:
6956-
case SyntaxKind.ConditionalExpression:
6957-
case SyntaxKind.BinaryExpression:
6958-
nodeStack.push({node, child});
6959-
break;
6960-
case SyntaxKind.SourceFile:
6961-
case SyntaxKind.ModuleDeclaration:
6962-
// Stop at the first containing file or module declaration
6963-
break loop;
6964-
}
6965-
if (node === top) {
6945+
const declaration = getDeclarationOfKind(symbol, SyntaxKind.VariableDeclaration);
6946+
const top = declaration && getDeclarationContainer(declaration);
6947+
const originalType = type;
6948+
const nodeStack: {node: Node, child: Node}[] = [];
6949+
loop: while (node.parent) {
6950+
const child = node;
6951+
node = node.parent;
6952+
switch (node.kind) {
6953+
case SyntaxKind.IfStatement:
6954+
case SyntaxKind.ConditionalExpression:
6955+
case SyntaxKind.BinaryExpression:
6956+
nodeStack.push({node, child});
69666957
break;
6967-
}
6958+
case SyntaxKind.SourceFile:
6959+
case SyntaxKind.ModuleDeclaration:
6960+
// Stop at the first containing file or module declaration
6961+
break loop;
6962+
}
6963+
if (node === top) {
6964+
break;
69686965
}
6966+
}
69696967

6970-
let nodes: {node: Node, child: Node};
6971-
while (nodes = nodeStack.pop()) {
6972-
const {node, child} = nodes;
6973-
switch (node.kind) {
6974-
case SyntaxKind.IfStatement:
6975-
// In a branch of an if statement, narrow based on controlling expression
6976-
if (child !== (<IfStatement>node).expression) {
6977-
type = narrowType(type, (<IfStatement>node).expression, /*assumeTrue*/ child === (<IfStatement>node).thenStatement);
6978-
}
6979-
break;
6980-
case SyntaxKind.ConditionalExpression:
6981-
// In a branch of a conditional expression, narrow based on controlling condition
6982-
if (child !== (<ConditionalExpression>node).condition) {
6983-
type = narrowType(type, (<ConditionalExpression>node).condition, /*assumeTrue*/ child === (<ConditionalExpression>node).whenTrue);
6968+
let nodes: {node: Node, child: Node};
6969+
while (nodes = nodeStack.pop()) {
6970+
const {node, child} = nodes;
6971+
switch (node.kind) {
6972+
case SyntaxKind.IfStatement:
6973+
// In a branch of an if statement, narrow based on controlling expression
6974+
if (child !== (<IfStatement>node).expression) {
6975+
type = narrowType(type, (<IfStatement>node).expression, /*assumeTrue*/ child === (<IfStatement>node).thenStatement);
6976+
}
6977+
break;
6978+
case SyntaxKind.ConditionalExpression:
6979+
// In a branch of a conditional expression, narrow based on controlling condition
6980+
if (child !== (<ConditionalExpression>node).condition) {
6981+
type = narrowType(type, (<ConditionalExpression>node).condition, /*assumeTrue*/ child === (<ConditionalExpression>node).whenTrue);
6982+
}
6983+
break;
6984+
case SyntaxKind.BinaryExpression:
6985+
// In the right operand of an && or ||, narrow based on left operand
6986+
if (child === (<BinaryExpression>node).right) {
6987+
if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) {
6988+
type = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ true);
69846989
}
6985-
break;
6986-
case SyntaxKind.BinaryExpression:
6987-
// In the right operand of an && or ||, narrow based on left operand
6988-
if (child === (<BinaryExpression>node).right) {
6989-
if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) {
6990-
type = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ true);
6991-
}
6992-
else if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken) {
6993-
type = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ false);
6994-
}
6990+
else if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken) {
6991+
type = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ false);
69956992
}
6996-
break;
6997-
default:
6998-
Debug.fail("Unreachable!");
6999-
}
7000-
7001-
// Use original type if construct contains assignments to variable
7002-
if (type !== originalType && isVariableAssignedWithin(symbol, node)) {
7003-
type = originalType;
7004-
}
6993+
}
6994+
break;
6995+
default:
6996+
Debug.fail("Unreachable!");
70056997
}
70066998

7007-
// Preserve old top-level behavior - if the branch is really an empty set, revert to prior type
7008-
if (type === emptyUnionType) {
6999+
// Use original type if construct contains assignments to variable
7000+
if (type !== originalType && isVariableAssignedWithin(symbol, node)) {
70097001
type = originalType;
70107002
}
70117003
}
7004+
7005+
// Preserve old top-level behavior - if the branch is really an empty set, revert to prior type
7006+
if (type === emptyUnionType) {
7007+
type = originalType;
7008+
}
70127009
}
70137010

70147011
return type;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//// [typeGuardNarrowsPrimitiveIntersection.ts]
2+
type Tag = {__tag: any};
3+
declare function isNonBlank(value: string) : value is (string & Tag);
4+
declare function doThis(value: string & Tag): void;
5+
declare function doThat(value: string) : void;
6+
let value: string;
7+
if (isNonBlank(value)) {
8+
doThis(value);
9+
} else {
10+
doThat(value);
11+
}
12+
13+
14+
const enum Tag2 {}
15+
declare function isNonBlank2(value: string) : value is (string & Tag2);
16+
declare function doThis2(value: string & Tag2): void;
17+
declare function doThat2(value: string) : void;
18+
if (isNonBlank2(value)) {
19+
doThis2(value);
20+
} else {
21+
doThat2(value);
22+
}
23+
24+
25+
//// [typeGuardNarrowsPrimitiveIntersection.js]
26+
var value;
27+
if (isNonBlank(value)) {
28+
doThis(value);
29+
}
30+
else {
31+
doThat(value);
32+
}
33+
if (isNonBlank2(value)) {
34+
doThis2(value);
35+
}
36+
else {
37+
doThat2(value);
38+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
=== tests/cases/conformance/expressions/typeGuards/typeGuardNarrowsPrimitiveIntersection.ts ===
2+
type Tag = {__tag: any};
3+
>Tag : Symbol(Tag, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 0, 0))
4+
>__tag : Symbol(__tag, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 0, 12))
5+
6+
declare function isNonBlank(value: string) : value is (string & Tag);
7+
>isNonBlank : Symbol(isNonBlank, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 0, 24))
8+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 1, 28))
9+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 1, 28))
10+
>Tag : Symbol(Tag, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 0, 0))
11+
12+
declare function doThis(value: string & Tag): void;
13+
>doThis : Symbol(doThis, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 1, 69))
14+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 2, 24))
15+
>Tag : Symbol(Tag, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 0, 0))
16+
17+
declare function doThat(value: string) : void;
18+
>doThat : Symbol(doThat, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 2, 51))
19+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 3, 24))
20+
21+
let value: string;
22+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 4, 3))
23+
24+
if (isNonBlank(value)) {
25+
>isNonBlank : Symbol(isNonBlank, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 0, 24))
26+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 4, 3))
27+
28+
doThis(value);
29+
>doThis : Symbol(doThis, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 1, 69))
30+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 4, 3))
31+
32+
} else {
33+
doThat(value);
34+
>doThat : Symbol(doThat, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 2, 51))
35+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 4, 3))
36+
}
37+
38+
39+
const enum Tag2 {}
40+
>Tag2 : Symbol(Tag2, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 9, 1))
41+
42+
declare function isNonBlank2(value: string) : value is (string & Tag2);
43+
>isNonBlank2 : Symbol(isNonBlank2, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 12, 18))
44+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 13, 29))
45+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 13, 29))
46+
>Tag2 : Symbol(Tag2, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 9, 1))
47+
48+
declare function doThis2(value: string & Tag2): void;
49+
>doThis2 : Symbol(doThis2, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 13, 71))
50+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 14, 25))
51+
>Tag2 : Symbol(Tag2, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 9, 1))
52+
53+
declare function doThat2(value: string) : void;
54+
>doThat2 : Symbol(doThat2, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 14, 53))
55+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 15, 25))
56+
57+
if (isNonBlank2(value)) {
58+
>isNonBlank2 : Symbol(isNonBlank2, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 12, 18))
59+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 4, 3))
60+
61+
doThis2(value);
62+
>doThis2 : Symbol(doThis2, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 13, 71))
63+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 4, 3))
64+
65+
} else {
66+
doThat2(value);
67+
>doThat2 : Symbol(doThat2, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 14, 53))
68+
>value : Symbol(value, Decl(typeGuardNarrowsPrimitiveIntersection.ts, 4, 3))
69+
}
70+
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
=== tests/cases/conformance/expressions/typeGuards/typeGuardNarrowsPrimitiveIntersection.ts ===
2+
type Tag = {__tag: any};
3+
>Tag : { __tag: any; }
4+
>__tag : any
5+
6+
declare function isNonBlank(value: string) : value is (string & Tag);
7+
>isNonBlank : (value: string) => value is string & { __tag: any; }
8+
>value : string
9+
>value : any
10+
>Tag : { __tag: any; }
11+
12+
declare function doThis(value: string & Tag): void;
13+
>doThis : (value: string & { __tag: any; }) => void
14+
>value : string & { __tag: any; }
15+
>Tag : { __tag: any; }
16+
17+
declare function doThat(value: string) : void;
18+
>doThat : (value: string) => void
19+
>value : string
20+
21+
let value: string;
22+
>value : string
23+
24+
if (isNonBlank(value)) {
25+
>isNonBlank(value) : boolean
26+
>isNonBlank : (value: string) => value is string & { __tag: any; }
27+
>value : string
28+
29+
doThis(value);
30+
>doThis(value) : void
31+
>doThis : (value: string & { __tag: any; }) => void
32+
>value : string & { __tag: any; }
33+
34+
} else {
35+
doThat(value);
36+
>doThat(value) : void
37+
>doThat : (value: string) => void
38+
>value : string
39+
}
40+
41+
42+
const enum Tag2 {}
43+
>Tag2 : Tag2
44+
45+
declare function isNonBlank2(value: string) : value is (string & Tag2);
46+
>isNonBlank2 : (value: string) => value is string & Tag2
47+
>value : string
48+
>value : any
49+
>Tag2 : Tag2
50+
51+
declare function doThis2(value: string & Tag2): void;
52+
>doThis2 : (value: string & Tag2) => void
53+
>value : string & Tag2
54+
>Tag2 : Tag2
55+
56+
declare function doThat2(value: string) : void;
57+
>doThat2 : (value: string) => void
58+
>value : string
59+
60+
if (isNonBlank2(value)) {
61+
>isNonBlank2(value) : boolean
62+
>isNonBlank2 : (value: string) => value is string & Tag2
63+
>value : string
64+
65+
doThis2(value);
66+
>doThis2(value) : void
67+
>doThis2 : (value: string & Tag2) => void
68+
>value : string & Tag2
69+
70+
} else {
71+
doThat2(value);
72+
>doThat2(value) : void
73+
>doThat2 : (value: string) => void
74+
>value : string
75+
}
76+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
type Tag = {__tag: any};
2+
declare function isNonBlank(value: string) : value is (string & Tag);
3+
declare function doThis(value: string & Tag): void;
4+
declare function doThat(value: string) : void;
5+
let value: string;
6+
if (isNonBlank(value)) {
7+
doThis(value);
8+
} else {
9+
doThat(value);
10+
}
11+
12+
13+
const enum Tag2 {}
14+
declare function isNonBlank2(value: string) : value is (string & Tag2);
15+
declare function doThis2(value: string & Tag2): void;
16+
declare function doThat2(value: string) : void;
17+
if (isNonBlank2(value)) {
18+
doThis2(value);
19+
} else {
20+
doThat2(value);
21+
}

0 commit comments

Comments
 (0)