Skip to content

Commit b3f0a71

Browse files
committed
Merge pull request #5442 from weswigham/empty-set
Improve type guard consistiency
2 parents beb998b + 5f18486 commit b3f0a71

22 files changed

+723
-123
lines changed

src/compiler/checker.ts

Lines changed: 70 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ namespace ts {
109109
const undefinedType = createIntrinsicType(TypeFlags.Undefined | TypeFlags.ContainsUndefinedOrNull, "undefined");
110110
const nullType = createIntrinsicType(TypeFlags.Null | TypeFlags.ContainsUndefinedOrNull, "null");
111111
const unknownType = createIntrinsicType(TypeFlags.Any, "unknown");
112-
const circularType = createIntrinsicType(TypeFlags.Any, "__circular__");
113112

114113
const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
114+
const emptyUnionType = emptyObjectType;
115115
const emptyGenericType = <GenericType><ObjectType>createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
116116
emptyGenericType.instantiations = {};
117117

@@ -4413,7 +4413,7 @@ namespace ts {
44134413
// a named type that circularly references itself.
44144414
function getUnionType(types: Type[], noSubtypeReduction?: boolean): Type {
44154415
if (types.length === 0) {
4416-
return emptyObjectType;
4416+
return emptyUnionType;
44174417
}
44184418
const typeSet: Type[] = [];
44194419
addTypesToSet(typeSet, types, TypeFlags.Union);
@@ -6285,27 +6285,6 @@ namespace ts {
62856285
Debug.fail("should not get here");
62866286
}
62876287

6288-
// For a union type, remove all constituent types that are of the given type kind (when isOfTypeKind is true)
6289-
// or not of the given type kind (when isOfTypeKind is false)
6290-
function removeTypesFromUnionType(type: Type, typeKind: TypeFlags, isOfTypeKind: boolean, allowEmptyUnionResult: boolean): Type {
6291-
if (type.flags & TypeFlags.Union) {
6292-
const types = (<UnionType>type).types;
6293-
if (forEach(types, t => !!(t.flags & typeKind) === isOfTypeKind)) {
6294-
// Above we checked if we have anything to remove, now use the opposite test to do the removal
6295-
const narrowedType = getUnionType(filter(types, t => !(t.flags & typeKind) === isOfTypeKind));
6296-
if (allowEmptyUnionResult || narrowedType !== emptyObjectType) {
6297-
return narrowedType;
6298-
}
6299-
}
6300-
}
6301-
else if (allowEmptyUnionResult && !!(type.flags & typeKind) === isOfTypeKind) {
6302-
// Use getUnionType(emptyArray) instead of emptyObjectType in case the way empty union types
6303-
// are represented ever changes.
6304-
return getUnionType(emptyArray);
6305-
}
6306-
return type;
6307-
}
6308-
63096288
function hasInitializer(node: VariableLikeDeclaration): boolean {
63106289
return !!(node.initializer || isBindingPattern(node.parent) && hasInitializer(<VariableLikeDeclaration>node.parent.parent));
63116290
}
@@ -6407,53 +6386,71 @@ namespace ts {
64076386
// Only narrow when symbol is variable of type any or an object, union, or type parameter type
64086387
if (node && symbol.flags & SymbolFlags.Variable) {
64096388
if (isTypeAny(type) || type.flags & (TypeFlags.ObjectType | TypeFlags.Union | TypeFlags.TypeParameter)) {
6389+
const originalType = type;
6390+
const nodeStack: {node: Node, child: Node}[] = [];
64106391
loop: while (node.parent) {
64116392
const child = node;
64126393
node = node.parent;
6413-
let narrowedType = type;
6394+
switch (node.kind) {
6395+
case SyntaxKind.IfStatement:
6396+
case SyntaxKind.ConditionalExpression:
6397+
case SyntaxKind.BinaryExpression:
6398+
nodeStack.push({node, child});
6399+
break;
6400+
case SyntaxKind.SourceFile:
6401+
case SyntaxKind.ModuleDeclaration:
6402+
case SyntaxKind.FunctionDeclaration:
6403+
case SyntaxKind.MethodDeclaration:
6404+
case SyntaxKind.MethodSignature:
6405+
case SyntaxKind.GetAccessor:
6406+
case SyntaxKind.SetAccessor:
6407+
case SyntaxKind.Constructor:
6408+
// Stop at the first containing function or module declaration
6409+
break loop;
6410+
}
6411+
}
6412+
6413+
let nodes: {node: Node, child: Node};
6414+
while (nodes = nodeStack.pop()) {
6415+
const {node, child} = nodes;
64146416
switch (node.kind) {
64156417
case SyntaxKind.IfStatement:
64166418
// In a branch of an if statement, narrow based on controlling expression
64176419
if (child !== (<IfStatement>node).expression) {
6418-
narrowedType = narrowType(type, (<IfStatement>node).expression, /*assumeTrue*/ child === (<IfStatement>node).thenStatement);
6420+
type = narrowType(type, (<IfStatement>node).expression, /*assumeTrue*/ child === (<IfStatement>node).thenStatement);
64196421
}
64206422
break;
64216423
case SyntaxKind.ConditionalExpression:
64226424
// In a branch of a conditional expression, narrow based on controlling condition
64236425
if (child !== (<ConditionalExpression>node).condition) {
6424-
narrowedType = narrowType(type, (<ConditionalExpression>node).condition, /*assumeTrue*/ child === (<ConditionalExpression>node).whenTrue);
6426+
type = narrowType(type, (<ConditionalExpression>node).condition, /*assumeTrue*/ child === (<ConditionalExpression>node).whenTrue);
64256427
}
64266428
break;
64276429
case SyntaxKind.BinaryExpression:
64286430
// In the right operand of an && or ||, narrow based on left operand
64296431
if (child === (<BinaryExpression>node).right) {
64306432
if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) {
6431-
narrowedType = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ true);
6433+
type = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ true);
64326434
}
64336435
else if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken) {
6434-
narrowedType = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ false);
6436+
type = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ false);
64356437
}
64366438
}
64376439
break;
6438-
case SyntaxKind.SourceFile:
6439-
case SyntaxKind.ModuleDeclaration:
6440-
case SyntaxKind.FunctionDeclaration:
6441-
case SyntaxKind.MethodDeclaration:
6442-
case SyntaxKind.MethodSignature:
6443-
case SyntaxKind.GetAccessor:
6444-
case SyntaxKind.SetAccessor:
6445-
case SyntaxKind.Constructor:
6446-
// Stop at the first containing function or module declaration
6447-
break loop;
6440+
default:
6441+
Debug.fail("Unreachable!");
64486442
}
6449-
// Use narrowed type if construct contains no assignments to variable
6450-
if (narrowedType !== type) {
6451-
if (isVariableAssignedWithin(symbol, node)) {
6452-
break;
6453-
}
6454-
type = narrowedType;
6443+
6444+
// Use original type if construct contains assignments to variable
6445+
if (type !== originalType && isVariableAssignedWithin(symbol, node)) {
6446+
type = originalType;
64556447
}
64566448
}
6449+
6450+
// Preserve old top-level behavior - if the branch is really an empty set, revert to prior type
6451+
if (type === emptyUnionType) {
6452+
type = originalType;
6453+
}
64576454
}
64586455
}
64596456

@@ -6469,31 +6466,31 @@ namespace ts {
64696466
if (left.expression.kind !== SyntaxKind.Identifier || getResolvedSymbol(<Identifier>left.expression) !== symbol) {
64706467
return type;
64716468
}
6472-
const typeInfo = primitiveTypeInfo[right.text];
64736469
if (expr.operatorToken.kind === SyntaxKind.ExclamationEqualsEqualsToken) {
64746470
assumeTrue = !assumeTrue;
64756471
}
6476-
if (assumeTrue) {
6477-
// Assumed result is true. If check was not for a primitive type, remove all primitive types
6478-
if (!typeInfo) {
6479-
return removeTypesFromUnionType(type, /*typeKind*/ TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.Boolean | TypeFlags.ESSymbol,
6480-
/*isOfTypeKind*/ true, /*allowEmptyUnionResult*/ false);
6481-
}
6482-
// Check was for a primitive type, return that primitive type if it is a subtype
6483-
if (isTypeSubtypeOf(typeInfo.type, type)) {
6484-
return typeInfo.type;
6485-
}
6486-
// Otherwise, remove all types that aren't of the primitive type kind. This can happen when the type is
6487-
// union of enum types and other types.
6488-
return removeTypesFromUnionType(type, /*typeKind*/ typeInfo.flags, /*isOfTypeKind*/ false, /*allowEmptyUnionResult*/ false);
6472+
const typeInfo = primitiveTypeInfo[right.text];
6473+
// If the type to be narrowed is any and we're checking a primitive with assumeTrue=true, return the primitive
6474+
if (!!(type.flags & TypeFlags.Any) && typeInfo && assumeTrue) {
6475+
return typeInfo.type;
6476+
}
6477+
let flags: TypeFlags;
6478+
if (typeInfo) {
6479+
flags = typeInfo.flags;
64896480
}
64906481
else {
6491-
// Assumed result is false. If check was for a primitive type, remove that primitive type
6492-
if (typeInfo) {
6493-
return removeTypesFromUnionType(type, /*typeKind*/ typeInfo.flags, /*isOfTypeKind*/ true, /*allowEmptyUnionResult*/ false);
6494-
}
6495-
// Otherwise we don't have enough information to do anything.
6496-
return type;
6482+
assumeTrue = !assumeTrue;
6483+
flags = TypeFlags.NumberLike | TypeFlags.StringLike | TypeFlags.ESSymbol | TypeFlags.Boolean;
6484+
}
6485+
// At this point we can bail if it's not a union
6486+
if (!(type.flags & TypeFlags.Union)) {
6487+
// If the active non-union type would be removed from a union by this type guard, return an empty union
6488+
return filterUnion(type) ? type : emptyUnionType;
6489+
}
6490+
return getUnionType(filter((type as UnionType).types, filterUnion), /*noSubtypeReduction*/ true);
6491+
6492+
function filterUnion(type: Type) {
6493+
return assumeTrue === !!(type.flags & flags);
64976494
}
64986495
}
64996496

@@ -6507,7 +6504,7 @@ namespace ts {
65076504
// and the second operand was false. We narrow with those assumptions and union the two resulting types.
65086505
return getUnionType([
65096506
narrowType(type, expr.left, /*assumeTrue*/ false),
6510-
narrowType(narrowType(type, expr.left, /*assumeTrue*/ true), expr.right, /*assumeTrue*/ false)
6507+
narrowType(type, expr.right, /*assumeTrue*/ false)
65116508
]);
65126509
}
65136510
}
@@ -6518,7 +6515,7 @@ namespace ts {
65186515
// and the second operand was true. We narrow with those assumptions and union the two resulting types.
65196516
return getUnionType([
65206517
narrowType(type, expr.left, /*assumeTrue*/ true),
6521-
narrowType(narrowType(type, expr.left, /*assumeTrue*/ false), expr.right, /*assumeTrue*/ true)
6518+
narrowType(type, expr.right, /*assumeTrue*/ true)
65226519
]);
65236520
}
65246521
else {
@@ -12736,9 +12733,14 @@ namespace ts {
1273612733

1273712734
// After we remove all types that are StringLike, we will know if there was a string constituent
1273812735
// based on whether the remaining type is the same as the initial type.
12739-
const arrayType = removeTypesFromUnionType(arrayOrStringType, TypeFlags.StringLike, /*isTypeOfKind*/ true, /*allowEmptyUnionResult*/ true);
12736+
let arrayType = arrayOrStringType;
12737+
if (arrayOrStringType.flags & TypeFlags.Union) {
12738+
arrayType = getUnionType(filter((arrayOrStringType as UnionType).types, t => !(t.flags & TypeFlags.StringLike)));
12739+
}
12740+
else if (arrayOrStringType.flags & TypeFlags.StringLike) {
12741+
arrayType = emptyUnionType;
12742+
}
1274012743
const hasStringConstituent = arrayOrStringType !== arrayType;
12741-
1274212744
let reportedError = false;
1274312745
if (hasStringConstituent) {
1274412746
if (languageVersion < ScriptTarget.ES5) {

tests/baselines/reference/symbolType18.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ if (typeof x === "object") {
2121
}
2222
else {
2323
x;
24-
>x : symbol | Foo
24+
>x : symbol
2525
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//// [typeGuardEnums.ts]
2+
enum E {}
3+
enum V {}
4+
5+
let x: number|string|E|V;
6+
7+
if (typeof x === "number") {
8+
x; // number|E|V
9+
}
10+
else {
11+
x; // string
12+
}
13+
14+
if (typeof x !== "number") {
15+
x; // string
16+
}
17+
else {
18+
x; // number|E|V
19+
}
20+
21+
22+
//// [typeGuardEnums.js]
23+
var E;
24+
(function (E) {
25+
})(E || (E = {}));
26+
var V;
27+
(function (V) {
28+
})(V || (V = {}));
29+
var x;
30+
if (typeof x === "number") {
31+
x; // number|E|V
32+
}
33+
else {
34+
x; // string
35+
}
36+
if (typeof x !== "number") {
37+
x; // string
38+
}
39+
else {
40+
x; // number|E|V
41+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
=== tests/cases/conformance/expressions/typeGuards/typeGuardEnums.ts ===
2+
enum E {}
3+
>E : Symbol(E, Decl(typeGuardEnums.ts, 0, 0))
4+
5+
enum V {}
6+
>V : Symbol(V, Decl(typeGuardEnums.ts, 0, 9))
7+
8+
let x: number|string|E|V;
9+
>x : Symbol(x, Decl(typeGuardEnums.ts, 3, 3))
10+
>E : Symbol(E, Decl(typeGuardEnums.ts, 0, 0))
11+
>V : Symbol(V, Decl(typeGuardEnums.ts, 0, 9))
12+
13+
if (typeof x === "number") {
14+
>x : Symbol(x, Decl(typeGuardEnums.ts, 3, 3))
15+
16+
x; // number|E|V
17+
>x : Symbol(x, Decl(typeGuardEnums.ts, 3, 3))
18+
}
19+
else {
20+
x; // string
21+
>x : Symbol(x, Decl(typeGuardEnums.ts, 3, 3))
22+
}
23+
24+
if (typeof x !== "number") {
25+
>x : Symbol(x, Decl(typeGuardEnums.ts, 3, 3))
26+
27+
x; // string
28+
>x : Symbol(x, Decl(typeGuardEnums.ts, 3, 3))
29+
}
30+
else {
31+
x; // number|E|V
32+
>x : Symbol(x, Decl(typeGuardEnums.ts, 3, 3))
33+
}
34+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
=== tests/cases/conformance/expressions/typeGuards/typeGuardEnums.ts ===
2+
enum E {}
3+
>E : E
4+
5+
enum V {}
6+
>V : V
7+
8+
let x: number|string|E|V;
9+
>x : number | string | E | V
10+
>E : E
11+
>V : V
12+
13+
if (typeof x === "number") {
14+
>typeof x === "number" : boolean
15+
>typeof x : string
16+
>x : number | string | E | V
17+
>"number" : string
18+
19+
x; // number|E|V
20+
>x : number | E | V
21+
}
22+
else {
23+
x; // string
24+
>x : string
25+
}
26+
27+
if (typeof x !== "number") {
28+
>typeof x !== "number" : boolean
29+
>typeof x : string
30+
>x : number | string | E | V
31+
>"number" : string
32+
33+
x; // string
34+
>x : string
35+
}
36+
else {
37+
x; // number|E|V
38+
>x : number | E | V
39+
}
40+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//// [typeGuardNesting.ts]
2+
let strOrBool: string|boolean;
3+
if ((typeof strOrBool === 'boolean' && !strOrBool) || typeof strOrBool === 'string') {
4+
let label: string = (typeof strOrBool === 'string') ? strOrBool : "string";
5+
let bool: boolean = (typeof strOrBool === 'boolean') ? strOrBool : false;
6+
let label2: string = (typeof strOrBool !== 'boolean') ? strOrBool : "string";
7+
let bool2: boolean = (typeof strOrBool !== 'string') ? strOrBool : false;
8+
}
9+
10+
if ((typeof strOrBool !== 'string' && !strOrBool) || typeof strOrBool !== 'boolean') {
11+
let label: string = (typeof strOrBool === 'string') ? strOrBool : "string";
12+
let bool: boolean = (typeof strOrBool === 'boolean') ? strOrBool : false;
13+
let label2: string = (typeof strOrBool !== 'boolean') ? strOrBool : "string";
14+
let bool2: boolean = (typeof strOrBool !== 'string') ? strOrBool : false;
15+
}
16+
17+
18+
//// [typeGuardNesting.js]
19+
var strOrBool;
20+
if ((typeof strOrBool === 'boolean' && !strOrBool) || typeof strOrBool === 'string') {
21+
var label = (typeof strOrBool === 'string') ? strOrBool : "string";
22+
var bool = (typeof strOrBool === 'boolean') ? strOrBool : false;
23+
var label2 = (typeof strOrBool !== 'boolean') ? strOrBool : "string";
24+
var bool2 = (typeof strOrBool !== 'string') ? strOrBool : false;
25+
}
26+
if ((typeof strOrBool !== 'string' && !strOrBool) || typeof strOrBool !== 'boolean') {
27+
var label = (typeof strOrBool === 'string') ? strOrBool : "string";
28+
var bool = (typeof strOrBool === 'boolean') ? strOrBool : false;
29+
var label2 = (typeof strOrBool !== 'boolean') ? strOrBool : "string";
30+
var bool2 = (typeof strOrBool !== 'string') ? strOrBool : false;
31+
}

0 commit comments

Comments
 (0)