Improve type narrowing algorithm with typeof

This commit is contained in:
Wesley Wigham 2015-10-28 16:06:40 -07:00
parent 062495c426
commit febda00f1b
12 changed files with 382 additions and 77 deletions

View file

@ -6338,6 +6338,10 @@ namespace ts {
// Stop at the first containing function or module declaration
break loop;
}
// Preserve old top-level behavior - if the branch is really an empty set, revert to prior type
if (narrowedType === getUnionType(emptyArray)) {
narrowedType = type;
}
// Use narrowed type if construct contains no assignments to variable
if (narrowedType !== type) {
if (isVariableAssignedWithin(symbol, node)) {
@ -6352,6 +6356,9 @@ namespace ts {
return type;
function narrowTypeByEquality(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
if (!(type.flags & TypeFlags.Union)) {
return type;
}
// Check that we have 'typeof <symbol>' on the left and string literal on the right
if (expr.left.kind !== SyntaxKind.TypeOfExpression || expr.right.kind !== SyntaxKind.StringLiteral) {
return type;
@ -6361,31 +6368,17 @@ namespace ts {
if (left.expression.kind !== SyntaxKind.Identifier || getResolvedSymbol(<Identifier>left.expression) !== symbol) {
return type;
}
let typeInfo = primitiveTypeInfo[right.text];
if (expr.operatorToken.kind === SyntaxKind.ExclamationEqualsEqualsToken) {
assumeTrue = !assumeTrue;
}
let typeInfo = primitiveTypeInfo[right.text];
let flags = typeInfo ? typeInfo.flags : (assumeTrue = !assumeTrue, TypeFlags.NumberLike | TypeFlags.StringLike | TypeFlags.ESSymbol | TypeFlags.Boolean);
let union = type as UnionType;
if (assumeTrue) {
// 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 | TypeFlags.ESSymbol,
/*isOfTypeKind*/ true, /*allowEmptyUnionResult*/ 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, /*allowEmptyUnionResult*/ true);
return getUnionType(filter(union.types, t => !!(t.flags & flags)));
}
else {
// 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, /*allowEmptyUnionResult*/ true);
}
// Otherwise we don't have enough information to do anything.
return type;
return getUnionType(filter(union.types, t => !(t.flags & flags)));
}
}
@ -6399,7 +6392,7 @@ namespace ts {
// and the second operand was false. We narrow with those assumptions and union the two resulting types.
return getUnionType([
narrowType(type, expr.left, /*assumeTrue*/ false),
narrowType(narrowType(type, expr.left, /*assumeTrue*/ true), expr.right, /*assumeTrue*/ false)
narrowType(type, expr.right, /*assumeTrue*/ false)
]);
}
}
@ -6410,7 +6403,7 @@ namespace ts {
// and the second operand was true. We narrow with those assumptions and union the two resulting types.
return getUnionType([
narrowType(type, expr.left, /*assumeTrue*/ true),
narrowType(narrowType(type, expr.left, /*assumeTrue*/ false), expr.right, /*assumeTrue*/ true)
narrowType(type, expr.right, /*assumeTrue*/ true)
]);
}
else {

View file

@ -21,5 +21,5 @@ if (typeof x === "object") {
}
else {
x;
>x : symbol | Foo
>x : symbol
}

View file

@ -0,0 +1,11 @@
//// [typeGuardNesting.ts]
let strOrBool: string|boolean;
if ((typeof strOrBool === 'boolean' && !strOrBool) || typeof strOrBool === 'string') {
var label: string = (typeof strOrBool === 'string') ? strOrBool : "other string";
}
//// [typeGuardNesting.js]
var strOrBool;
if ((typeof strOrBool === 'boolean' && !strOrBool) || typeof strOrBool === 'string') {
var label = (typeof strOrBool === 'string') ? strOrBool : "other string";
}

View file

@ -0,0 +1,14 @@
=== tests/cases/conformance/expressions/typeGuards/typeGuardNesting.ts ===
let strOrBool: string|boolean;
>strOrBool : Symbol(strOrBool, Decl(typeGuardNesting.ts, 0, 3))
if ((typeof strOrBool === 'boolean' && !strOrBool) || typeof strOrBool === 'string') {
>strOrBool : Symbol(strOrBool, Decl(typeGuardNesting.ts, 0, 3))
>strOrBool : Symbol(strOrBool, Decl(typeGuardNesting.ts, 0, 3))
>strOrBool : Symbol(strOrBool, Decl(typeGuardNesting.ts, 0, 3))
var label: string = (typeof strOrBool === 'string') ? strOrBool : "other string";
>label : Symbol(label, Decl(typeGuardNesting.ts, 2, 4))
>strOrBool : Symbol(strOrBool, Decl(typeGuardNesting.ts, 0, 3))
>strOrBool : Symbol(strOrBool, Decl(typeGuardNesting.ts, 0, 3))
}

View file

@ -0,0 +1,30 @@
=== tests/cases/conformance/expressions/typeGuards/typeGuardNesting.ts ===
let strOrBool: string|boolean;
>strOrBool : string | boolean
if ((typeof strOrBool === 'boolean' && !strOrBool) || typeof strOrBool === 'string') {
>(typeof strOrBool === 'boolean' && !strOrBool) || typeof strOrBool === 'string' : boolean
>(typeof strOrBool === 'boolean' && !strOrBool) : boolean
>typeof strOrBool === 'boolean' && !strOrBool : boolean
>typeof strOrBool === 'boolean' : boolean
>typeof strOrBool : string
>strOrBool : string | boolean
>'boolean' : string
>!strOrBool : boolean
>strOrBool : boolean
>typeof strOrBool === 'string' : boolean
>typeof strOrBool : string
>strOrBool : string | boolean
>'string' : string
var label: string = (typeof strOrBool === 'string') ? strOrBool : "other string";
>label : string
>(typeof strOrBool === 'string') ? strOrBool : "other string" : string
>(typeof strOrBool === 'string') : boolean
>typeof strOrBool === 'string' : boolean
>typeof strOrBool : string
>strOrBool : boolean | string
>'string' : string
>strOrBool : string
>"other string" : string
}

View file

@ -63,7 +63,7 @@ else {
var r2: string | C = strOrC; // string | C
>r2 : string | C
>C : C
>strOrC : string | C
>strOrC : string
}
if (typeof numOrC === "Object") {
>typeof numOrC === "Object" : boolean
@ -80,7 +80,7 @@ else {
var r3: number | C = numOrC; // number | C
>r3 : number | C
>C : C
>numOrC : number | C
>numOrC : number
}
if (typeof boolOrC === "Object") {
>typeof boolOrC === "Object" : boolean
@ -97,7 +97,7 @@ else {
var r4: boolean | C = boolOrC; // boolean | C
>r4 : boolean | C
>C : C
>boolOrC : boolean | C
>boolOrC : boolean
}
// Narrowing occurs only if target type is a subtype of variable type
@ -129,7 +129,7 @@ if (typeof strOrC !== "Object") {
var r2: string | C = strOrC; // string | C
>r2 : string | C
>C : C
>strOrC : string | C
>strOrC : string
}
else {
c = strOrC; // C
@ -146,7 +146,7 @@ if (typeof numOrC !== "Object") {
var r3: number | C = numOrC; // number | C
>r3 : number | C
>C : C
>numOrC : number | C
>numOrC : number
}
else {
c = numOrC; // C
@ -163,7 +163,7 @@ if (typeof boolOrC !== "Object") {
var r4: boolean | C = boolOrC; // boolean | C
>r4 : boolean | C
>C : C
>boolOrC : boolean | C
>boolOrC : boolean
}
else {
c = boolOrC; // C

View file

@ -0,0 +1,17 @@
//// [typeGuardRedundancy.ts]
var x: string|number;
var r1 = typeof x === "string" && typeof x === "string" ? x.substr : x.toFixed;
var r2 = !(typeof x === "string" && typeof x === "string") ? x.toFixed : x.substr;
var r3 = typeof x === "string" || typeof x === "string" ? x.substr : x.toFixed;
var r4 = !(typeof x === "string" || typeof x === "string") ? x.toFixed : x.substr;
//// [typeGuardRedundancy.js]
var x;
var r1 = typeof x === "string" && typeof x === "string" ? x.substr : x.toFixed;
var r2 = !(typeof x === "string" && typeof x === "string") ? x.toFixed : x.substr;
var r3 = typeof x === "string" || typeof x === "string" ? x.substr : x.toFixed;
var r4 = !(typeof x === "string" || typeof x === "string") ? x.toFixed : x.substr;

View file

@ -0,0 +1,48 @@
=== tests/cases/conformance/expressions/typeGuards/typeGuardRedundancy.ts ===
var x: string|number;
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
var r1 = typeof x === "string" && typeof x === "string" ? x.substr : x.toFixed;
>r1 : Symbol(r1, Decl(typeGuardRedundancy.ts, 2, 3))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>x.substr : Symbol(String.substr, Decl(lib.d.ts, --, --))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>substr : Symbol(String.substr, Decl(lib.d.ts, --, --))
>x.toFixed : Symbol(Number.toFixed, Decl(lib.d.ts, --, --))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>toFixed : Symbol(Number.toFixed, Decl(lib.d.ts, --, --))
var r2 = !(typeof x === "string" && typeof x === "string") ? x.toFixed : x.substr;
>r2 : Symbol(r2, Decl(typeGuardRedundancy.ts, 4, 3))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>x.toFixed : Symbol(Number.toFixed, Decl(lib.d.ts, --, --))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>toFixed : Symbol(Number.toFixed, Decl(lib.d.ts, --, --))
>x.substr : Symbol(String.substr, Decl(lib.d.ts, --, --))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>substr : Symbol(String.substr, Decl(lib.d.ts, --, --))
var r3 = typeof x === "string" || typeof x === "string" ? x.substr : x.toFixed;
>r3 : Symbol(r3, Decl(typeGuardRedundancy.ts, 6, 3))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>x.substr : Symbol(String.substr, Decl(lib.d.ts, --, --))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>substr : Symbol(String.substr, Decl(lib.d.ts, --, --))
>x.toFixed : Symbol(Number.toFixed, Decl(lib.d.ts, --, --))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>toFixed : Symbol(Number.toFixed, Decl(lib.d.ts, --, --))
var r4 = !(typeof x === "string" || typeof x === "string") ? x.toFixed : x.substr;
>r4 : Symbol(r4, Decl(typeGuardRedundancy.ts, 8, 3))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>x.toFixed : Symbol(Number.toFixed, Decl(lib.d.ts, --, --))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>toFixed : Symbol(Number.toFixed, Decl(lib.d.ts, --, --))
>x.substr : Symbol(String.substr, Decl(lib.d.ts, --, --))
>x : Symbol(x, Decl(typeGuardRedundancy.ts, 0, 3))
>substr : Symbol(String.substr, Decl(lib.d.ts, --, --))

View file

@ -0,0 +1,84 @@
=== tests/cases/conformance/expressions/typeGuards/typeGuardRedundancy.ts ===
var x: string|number;
>x : string | number
var r1 = typeof x === "string" && typeof x === "string" ? x.substr : x.toFixed;
>r1 : (from: number, length?: number) => string
>typeof x === "string" && typeof x === "string" ? x.substr : x.toFixed : (from: number, length?: number) => string
>typeof x === "string" && typeof x === "string" : boolean
>typeof x === "string" : boolean
>typeof x : string
>x : string | number
>"string" : string
>typeof x === "string" : boolean
>typeof x : string
>x : string
>"string" : string
>x.substr : (from: number, length?: number) => string
>x : string
>substr : (from: number, length?: number) => string
>x.toFixed : (fractionDigits?: number) => string
>x : number
>toFixed : (fractionDigits?: number) => string
var r2 = !(typeof x === "string" && typeof x === "string") ? x.toFixed : x.substr;
>r2 : (fractionDigits?: number) => string
>!(typeof x === "string" && typeof x === "string") ? x.toFixed : x.substr : (fractionDigits?: number) => string
>!(typeof x === "string" && typeof x === "string") : boolean
>(typeof x === "string" && typeof x === "string") : boolean
>typeof x === "string" && typeof x === "string" : boolean
>typeof x === "string" : boolean
>typeof x : string
>x : string | number
>"string" : string
>typeof x === "string" : boolean
>typeof x : string
>x : string
>"string" : string
>x.toFixed : (fractionDigits?: number) => string
>x : number
>toFixed : (fractionDigits?: number) => string
>x.substr : (from: number, length?: number) => string
>x : string
>substr : (from: number, length?: number) => string
var r3 = typeof x === "string" || typeof x === "string" ? x.substr : x.toFixed;
>r3 : (from: number, length?: number) => string
>typeof x === "string" || typeof x === "string" ? x.substr : x.toFixed : (from: number, length?: number) => string
>typeof x === "string" || typeof x === "string" : boolean
>typeof x === "string" : boolean
>typeof x : string
>x : string | number
>"string" : string
>typeof x === "string" : boolean
>typeof x : string
>x : number
>"string" : string
>x.substr : (from: number, length?: number) => string
>x : string
>substr : (from: number, length?: number) => string
>x.toFixed : (fractionDigits?: number) => string
>x : number
>toFixed : (fractionDigits?: number) => string
var r4 = !(typeof x === "string" || typeof x === "string") ? x.toFixed : x.substr;
>r4 : (fractionDigits?: number) => string
>!(typeof x === "string" || typeof x === "string") ? x.toFixed : x.substr : (fractionDigits?: number) => string
>!(typeof x === "string" || typeof x === "string") : boolean
>(typeof x === "string" || typeof x === "string") : boolean
>typeof x === "string" || typeof x === "string" : boolean
>typeof x === "string" : boolean
>typeof x : string
>x : string | number
>"string" : string
>typeof x === "string" : boolean
>typeof x : string
>x : number
>"string" : string
>x.toFixed : (fractionDigits?: number) => string
>x : number
>toFixed : (fractionDigits?: number) => string
>x.substr : (from: number, length?: number) => string
>x : string
>substr : (from: number, length?: number) => string

View file

@ -1,49 +0,0 @@
tests/cases/conformance/expressions/typeGuards/typeGuardsWithAny.ts(11,7): error TS2339: Property 'p' does not exist on type 'string'.
tests/cases/conformance/expressions/typeGuards/typeGuardsWithAny.ts(18,7): error TS2339: Property 'p' does not exist on type 'number'.
tests/cases/conformance/expressions/typeGuards/typeGuardsWithAny.ts(25,7): error TS2339: Property 'p' does not exist on type 'boolean'.
==== tests/cases/conformance/expressions/typeGuards/typeGuardsWithAny.ts (3 errors) ====
var x: any = { p: 0 };
if (x instanceof Object) {
x.p; // No error, type any unaffected by instanceof type guard
}
else {
x.p; // No error, type any unaffected by instanceof type guard
}
if (typeof x === "string") {
x.p; // Error, type any narrowed by primitive type check
~
!!! error TS2339: Property 'p' does not exist on type 'string'.
}
else {
x.p; // No error, type unaffected in this branch
}
if (typeof x === "number") {
x.p; // Error, type any narrowed by primitive type check
~
!!! error TS2339: Property 'p' does not exist on type 'number'.
}
else {
x.p; // No error, type unaffected in this branch
}
if (typeof x === "boolean") {
x.p; // Error, type any narrowed by primitive type check
~
!!! error TS2339: Property 'p' does not exist on type 'boolean'.
}
else {
x.p; // No error, type unaffected in this branch
}
if (typeof x === "object") {
x.p; // No error, type any only affected by primitive type check
}
else {
x.p; // No error, type unaffected in this branch
}

View file

@ -0,0 +1,61 @@
=== tests/cases/conformance/expressions/typeGuards/typeGuardsWithAny.ts ===
var x: any = { p: 0 };
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
>p : Symbol(p, Decl(typeGuardsWithAny.ts, 0, 14))
if (x instanceof Object) {
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
>Object : Symbol(Object, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
x.p; // No error, type any unaffected by instanceof type guard
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
}
else {
x.p; // No error, type any unaffected by instanceof type guard
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
}
if (typeof x === "string") {
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
x.p; // Error, type any narrowed by primitive type check
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
}
else {
x.p; // No error, type unaffected in this branch
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
}
if (typeof x === "number") {
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
x.p; // Error, type any narrowed by primitive type check
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
}
else {
x.p; // No error, type unaffected in this branch
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
}
if (typeof x === "boolean") {
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
x.p; // Error, type any narrowed by primitive type check
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
}
else {
x.p; // No error, type unaffected in this branch
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
}
if (typeof x === "object") {
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
x.p; // No error, type any only affected by primitive type check
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
}
else {
x.p; // No error, type unaffected in this branch
>x : Symbol(x, Decl(typeGuardsWithAny.ts, 0, 3))
}

View file

@ -0,0 +1,96 @@
=== tests/cases/conformance/expressions/typeGuards/typeGuardsWithAny.ts ===
var x: any = { p: 0 };
>x : any
>{ p: 0 } : { p: number; }
>p : number
>0 : number
if (x instanceof Object) {
>x instanceof Object : boolean
>x : any
>Object : ObjectConstructor
x.p; // No error, type any unaffected by instanceof type guard
>x.p : any
>x : any
>p : any
}
else {
x.p; // No error, type any unaffected by instanceof type guard
>x.p : any
>x : any
>p : any
}
if (typeof x === "string") {
>typeof x === "string" : boolean
>typeof x : string
>x : any
>"string" : string
x.p; // Error, type any narrowed by primitive type check
>x.p : any
>x : any
>p : any
}
else {
x.p; // No error, type unaffected in this branch
>x.p : any
>x : any
>p : any
}
if (typeof x === "number") {
>typeof x === "number" : boolean
>typeof x : string
>x : any
>"number" : string
x.p; // Error, type any narrowed by primitive type check
>x.p : any
>x : any
>p : any
}
else {
x.p; // No error, type unaffected in this branch
>x.p : any
>x : any
>p : any
}
if (typeof x === "boolean") {
>typeof x === "boolean" : boolean
>typeof x : string
>x : any
>"boolean" : string
x.p; // Error, type any narrowed by primitive type check
>x.p : any
>x : any
>p : any
}
else {
x.p; // No error, type unaffected in this branch
>x.p : any
>x : any
>p : any
}
if (typeof x === "object") {
>typeof x === "object" : boolean
>typeof x : string
>x : any
>"object" : string
x.p; // No error, type any only affected by primitive type check
>x.p : any
>x : any
>p : any
}
else {
x.p; // No error, type unaffected in this branch
>x.p : any
>x : any
>p : any
}