Skip to content

Fixes to union types in type guards and instanceof #1803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 27, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 46 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ module ts {
var diagnostics: Diagnostic[] = [];
var diagnosticsModified: boolean = false;

var primitiveTypeInfo: Map<{ type: Type; flags: TypeFlags }> = {
"string": {
type: stringType,
flags: TypeFlags.StringLike
},
"number": {
type: numberType,
flags: TypeFlags.NumberLike
},
"boolean": {
type: booleanType,
flags: TypeFlags.Boolean
}
};

function addDiagnostic(diagnostic: Diagnostic) {
diagnostics.push(diagnostic);
diagnosticsModified = true;
Expand Down Expand Up @@ -4454,12 +4469,17 @@ module ts {
Debug.fail("should not get here");
}

// Remove one or more primitive types from a union type
function subtractPrimitiveTypes(type: Type, subtractMask: TypeFlags): Type {
// For a union type, remove all constituent types that are of the given type kind (when isOfTypeKind is true)
// or not of the given type kind (when isOfTypeKind is false)
function removeTypesFromUnionType(type: Type, typeKind: TypeFlags, isOfTypeKind: boolean): Type {
if (type.flags & TypeFlags.Union) {
var types = (<UnionType>type).types;
if (forEach(types, t => t.flags & subtractMask)) {
return getUnionType(filter(types, t => !(t.flags & subtractMask)));
if (forEach(types, t => !!(t.flags & typeKind) === isOfTypeKind)) {
// Above we checked if we have anything to remove, now use the opposite test to do the removal
var narrowedType = getUnionType(filter(types, t => !(t.flags & typeKind) === isOfTypeKind));
if (narrowedType !== emptyObjectType) {
return narrowedType;
}
}
}
return type;
Expand Down Expand Up @@ -4635,8 +4655,8 @@ module ts {
// Stop at the first containing function or module declaration
break loop;
}
// Use narrowed type if it is a subtype and construct contains no assignments to variable
if (narrowedType !== type && isTypeSubtypeOf(narrowedType, type)) {
// Use narrowed type if construct contains no assignments to variable
if (narrowedType !== type) {
if (isVariableAssignedWithin(symbol, node)) {
break;
}
Expand All @@ -4656,20 +4676,30 @@ module ts {
if (left.expression.kind !== SyntaxKind.Identifier || getResolvedSymbol(<Identifier>left.expression) !== symbol) {
return type;
}
var t = right.text;
var checkType: Type = t === "string" ? stringType : t === "number" ? numberType : t === "boolean" ? booleanType : emptyObjectType;
var typeInfo = primitiveTypeInfo[right.text];
if (expr.operator === SyntaxKind.ExclamationEqualsEqualsToken) {
assumeTrue = !assumeTrue;
}
if (assumeTrue) {
// The assumed result is true. If check was for a primitive type, that type is the narrowed type. Otherwise we can
// remove the primitive types from the narrowed type.
return checkType === emptyObjectType ? subtractPrimitiveTypes(type, TypeFlags.String | TypeFlags.Number | TypeFlags.Boolean) : checkType;
// Assumed result is true. If check was not for a primitive type, remove all primitive types
if (!typeInfo) {
return removeTypesFromUnionType(type, /*typeKind*/ TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.Boolean, /*isOfTypeKind*/ true);
}
// Check was for a primitive type, return that primitive type if it is a subtype
if (isTypeSubtypeOf(typeInfo.type, type)) {
return typeInfo.type;
}
// Otherwise, remove all types that aren't of the primitive type kind. This can happen when the type is
// union of enum types and other types.
return removeTypesFromUnionType(type, /*typeKind*/ typeInfo.flags, /*isOfTypeKind*/ false);
}
else {
// The assumed result is false. If check was for a primitive type we can remove that type from the narrowed type.
// Assumed result is false. If check was for a primitive type, remove that primitive type
if (typeInfo) {
return removeTypesFromUnionType(type, /*typeKind*/ typeInfo.flags, /*isOfTypeKind*/ true);
}
// Otherwise we don't have enough information to do anything.
return checkType === emptyObjectType ? type : subtractPrimitiveTypes(type, checkType.flags);
return type;
}
}

Expand Down Expand Up @@ -4730,7 +4760,8 @@ module ts {
return type;
}

// Narrow the given type based on the given expression having the assumed boolean value
// Narrow the given type based on the given expression having the assumed boolean value. The returned type
// will be a subtype or the same type as the argument.
function narrowType(type: Type, expr: Expression, assumeTrue: boolean): Type {
switch (expr.kind) {
case SyntaxKind.ParenthesizedExpression:
Expand Down Expand Up @@ -6749,7 +6780,7 @@ module ts {
// and the right operand to be of type Any or a subtype of the 'Function' interface type.
// The result is always of the Boolean primitive type.
// NOTE: do not raise error if leftType is unknown as related error was already reported
if (!isTypeOfKind(leftType, TypeFlags.Any | TypeFlags.ObjectType | TypeFlags.TypeParameter)) {
if (isTypeOfKind(leftType, TypeFlags.Primitive)) {
error(node.left, Diagnostics.The_left_hand_side_of_an_instanceof_expression_must_be_of_type_any_an_object_type_or_a_type_parameter);
}
// NOTE: do not raise error if right is unknown as related error was already reported
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,7 @@ module ts {
Unwidened = 0x00020000, // Unwidened type (is or contains Undefined or Null type)

Intrinsic = Any | String | Number | Boolean | Void | Undefined | Null,
Primitive = String | Number | Boolean | Void | Undefined | Null | StringLiteral | Enum,
StringLike = String | StringLiteral,
NumberLike = Number | Enum,
ObjectType = Class | Interface | Reference | Tuple | Anonymous,
Expand Down
77 changes: 77 additions & 0 deletions tests/baselines/reference/TypeGuardWithEnumUnion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//// [TypeGuardWithEnumUnion.ts]
enum Color { R, G, B }

function f1(x: Color | string) {
if (typeof x === "number") {
var y = x;
var y: Color;
}
else {
var z = x;
var z: string;
}
}

function f2(x: Color | string | string[]) {
if (typeof x === "object") {
var y = x;
var y: string[];
}
if (typeof x === "number") {
var z = x;
var z: Color;
}
else {
var w = x;
var w: string | string[];
}
if (typeof x === "string") {
var a = x;
var a: string;
}
else {
var b = x;
var b: Color | string[];
}
}


//// [TypeGuardWithEnumUnion.js]
var Color;
(function (Color) {
Color[Color["R"] = 0] = "R";
Color[Color["G"] = 1] = "G";
Color[Color["B"] = 2] = "B";
})(Color || (Color = {}));
function f1(x) {
if (typeof x === "number") {
var y = x;
var y;
}
else {
var z = x;
var z;
}
}
function f2(x) {
if (typeof x === "object") {
var y = x;
var y;
}
if (typeof x === "number") {
var z = x;
var z;
}
else {
var w = x;
var w;
}
if (typeof x === "string") {
var a = x;
var a;
}
else {
var b = x;
var b;
}
}
96 changes: 96 additions & 0 deletions tests/baselines/reference/TypeGuardWithEnumUnion.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
=== tests/cases/conformance/expressions/typeGuards/TypeGuardWithEnumUnion.ts ===
enum Color { R, G, B }
>Color : Color
>R : Color
>G : Color
>B : Color

function f1(x: Color | string) {
>f1 : (x: string | Color) => void
>x : string | Color
>Color : Color

if (typeof x === "number") {
>typeof x === "number" : boolean
>typeof x : string
>x : string | Color

var y = x;
>y : Color
>x : Color

var y: Color;
>y : Color
>Color : Color
}
else {
var z = x;
>z : string
>x : string

var z: string;
>z : string
}
}

function f2(x: Color | string | string[]) {
>f2 : (x: string | string[] | Color) => void
>x : string | string[] | Color
>Color : Color

if (typeof x === "object") {
>typeof x === "object" : boolean
>typeof x : string
>x : string | string[] | Color

var y = x;
>y : string[]
>x : string[]

var y: string[];
>y : string[]
}
if (typeof x === "number") {
>typeof x === "number" : boolean
>typeof x : string
>x : string | string[] | Color

var z = x;
>z : Color
>x : Color

var z: Color;
>z : Color
>Color : Color
}
else {
var w = x;
>w : string | string[]
>x : string | string[]

var w: string | string[];
>w : string | string[]
}
if (typeof x === "string") {
>typeof x === "string" : boolean
>typeof x : string
>x : string | string[] | Color

var a = x;
>a : string
>x : string

var a: string;
>a : string
}
else {
var b = x;
>b : string[] | Color
>x : string[] | Color

var b: Color | string[];
>b : string[] | Color
>Color : Color
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ var x2: Function;
var a: {};
var b: Object;
var c: C;
var d: string | C;

var r1 = a instanceof x1;
var r2 = b instanceof x2;
var r3 = c instanceof x1;
var r3 = c instanceof x1;
var r4 = d instanceof x1;


//// [instanceofOperatorWithLHSIsObject.js]
var C = (function () {
Expand All @@ -23,6 +26,8 @@ var x2;
var a;
var b;
var c;
var d;
var r1 = a instanceof x1;
var r2 = b instanceof x2;
var r3 = c instanceof x1;
var r4 = d instanceof x1;
10 changes: 10 additions & 0 deletions tests/baselines/reference/instanceofOperatorWithLHSIsObject.types
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ var c: C;
>c : C
>C : C

var d: string | C;
>d : string | C
>C : C

var r1 = a instanceof x1;
>r1 : boolean
>a instanceof x1 : boolean
Expand All @@ -38,3 +42,9 @@ var r3 = c instanceof x1;
>c : C
>x1 : any

var r4 = d instanceof x1;
>r4 : boolean
>d instanceof x1 : boolean
>d : string | C
>x1 : any

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ var x2: Function;
var a: {};
var b: Object;
var c: C;
var d: string | C;

var r1 = a instanceof x1;
var r2 = b instanceof x2;
var r3 = c instanceof x1;
var r3 = c instanceof x1;
var r4 = d instanceof x1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
enum Color { R, G, B }

function f1(x: Color | string) {
if (typeof x === "number") {
var y = x;
var y: Color;
}
else {
var z = x;
var z: string;
}
}

function f2(x: Color | string | string[]) {
if (typeof x === "object") {
var y = x;
var y: string[];
}
if (typeof x === "number") {
var z = x;
var z: Color;
}
else {
var w = x;
var w: string | string[];
}
if (typeof x === "string") {
var a = x;
var a: string;
}
else {
var b = x;
var b: Color | string[];
}
}