Merge pull request #5442 from weswigham/empty-set

Improve type guard consistiency
This commit is contained in:
Wesley Wigham
2015-11-11 12:52:37 -08:00
22 changed files with 727 additions and 127 deletions

View File

@@ -109,9 +109,9 @@ namespace ts {
const undefinedType = createIntrinsicType(TypeFlags.Undefined | TypeFlags.ContainsUndefinedOrNull, "undefined");
const nullType = createIntrinsicType(TypeFlags.Null | TypeFlags.ContainsUndefinedOrNull, "null");
const unknownType = createIntrinsicType(TypeFlags.Any, "unknown");
const circularType = createIntrinsicType(TypeFlags.Any, "__circular__");
const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
const emptyUnionType = emptyObjectType;
const emptyGenericType = <GenericType><ObjectType>createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
emptyGenericType.instantiations = {};
@@ -4413,7 +4413,7 @@ namespace ts {
// a named type that circularly references itself.
function getUnionType(types: Type[], noSubtypeReduction?: boolean): Type {
if (types.length === 0) {
return emptyObjectType;
return emptyUnionType;
}
const typeSet: Type[] = [];
addTypesToSet(typeSet, types, TypeFlags.Union);
@@ -6285,27 +6285,6 @@ namespace ts {
Debug.fail("should not get here");
}
// 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, allowEmptyUnionResult: boolean): Type {
if (type.flags & TypeFlags.Union) {
const types = (<UnionType>type).types;
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
const narrowedType = getUnionType(filter(types, t => !(t.flags & typeKind) === isOfTypeKind));
if (allowEmptyUnionResult || narrowedType !== emptyObjectType) {
return narrowedType;
}
}
}
else if (allowEmptyUnionResult && !!(type.flags & typeKind) === isOfTypeKind) {
// Use getUnionType(emptyArray) instead of emptyObjectType in case the way empty union types
// are represented ever changes.
return getUnionType(emptyArray);
}
return type;
}
function hasInitializer(node: VariableLikeDeclaration): boolean {
return !!(node.initializer || isBindingPattern(node.parent) && hasInitializer(<VariableLikeDeclaration>node.parent.parent));
}
@@ -6407,33 +6386,16 @@ namespace ts {
// Only narrow when symbol is variable of type any or an object, union, or type parameter type
if (node && symbol.flags & SymbolFlags.Variable) {
if (isTypeAny(type) || type.flags & (TypeFlags.ObjectType | TypeFlags.Union | TypeFlags.TypeParameter)) {
const originalType = type;
const nodeStack: {node: Node, child: Node}[] = [];
loop: while (node.parent) {
const child = node;
node = node.parent;
let narrowedType = type;
switch (node.kind) {
case SyntaxKind.IfStatement:
// In a branch of an if statement, narrow based on controlling expression
if (child !== (<IfStatement>node).expression) {
narrowedType = narrowType(type, (<IfStatement>node).expression, /*assumeTrue*/ child === (<IfStatement>node).thenStatement);
}
break;
case SyntaxKind.ConditionalExpression:
// In a branch of a conditional expression, narrow based on controlling condition
if (child !== (<ConditionalExpression>node).condition) {
narrowedType = narrowType(type, (<ConditionalExpression>node).condition, /*assumeTrue*/ child === (<ConditionalExpression>node).whenTrue);
}
break;
case SyntaxKind.BinaryExpression:
// In the right operand of an && or ||, narrow based on left operand
if (child === (<BinaryExpression>node).right) {
if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) {
narrowedType = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ true);
}
else if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken) {
narrowedType = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ false);
}
}
nodeStack.push({node, child});
break;
case SyntaxKind.SourceFile:
case SyntaxKind.ModuleDeclaration:
@@ -6446,13 +6408,48 @@ namespace ts {
// Stop at the first containing function or module declaration
break loop;
}
// Use narrowed type if construct contains no assignments to variable
if (narrowedType !== type) {
if (isVariableAssignedWithin(symbol, node)) {
}
let nodes: {node: Node, child: Node};
while (nodes = nodeStack.pop()) {
const {node, child} = nodes;
switch (node.kind) {
case SyntaxKind.IfStatement:
// In a branch of an if statement, narrow based on controlling expression
if (child !== (<IfStatement>node).expression) {
type = narrowType(type, (<IfStatement>node).expression, /*assumeTrue*/ child === (<IfStatement>node).thenStatement);
}
break;
}
type = narrowedType;
case SyntaxKind.ConditionalExpression:
// In a branch of a conditional expression, narrow based on controlling condition
if (child !== (<ConditionalExpression>node).condition) {
type = narrowType(type, (<ConditionalExpression>node).condition, /*assumeTrue*/ child === (<ConditionalExpression>node).whenTrue);
}
break;
case SyntaxKind.BinaryExpression:
// In the right operand of an && or ||, narrow based on left operand
if (child === (<BinaryExpression>node).right) {
if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) {
type = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ true);
}
else if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken) {
type = narrowType(type, (<BinaryExpression>node).left, /*assumeTrue*/ false);
}
}
break;
default:
Debug.fail("Unreachable!");
}
// Use original type if construct contains assignments to variable
if (type !== originalType && isVariableAssignedWithin(symbol, node)) {
type = originalType;
}
}
// Preserve old top-level behavior - if the branch is really an empty set, revert to prior type
if (type === emptyUnionType) {
type = originalType;
}
}
}
@@ -6469,31 +6466,31 @@ namespace ts {
if (left.expression.kind !== SyntaxKind.Identifier || getResolvedSymbol(<Identifier>left.expression) !== symbol) {
return type;
}
const typeInfo = primitiveTypeInfo[right.text];
if (expr.operatorToken.kind === SyntaxKind.ExclamationEqualsEqualsToken) {
assumeTrue = !assumeTrue;
}
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*/ false);
}
// 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*/ false);
const typeInfo = primitiveTypeInfo[right.text];
// If the type to be narrowed is any and we're checking a primitive with assumeTrue=true, return the primitive
if (!!(type.flags & TypeFlags.Any) && typeInfo && assumeTrue) {
return typeInfo.type;
}
let flags: TypeFlags;
if (typeInfo) {
flags = typeInfo.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*/ false);
}
// Otherwise we don't have enough information to do anything.
return type;
assumeTrue = !assumeTrue;
flags = TypeFlags.NumberLike | TypeFlags.StringLike | TypeFlags.ESSymbol | TypeFlags.Boolean;
}
// At this point we can bail if it's not a union
if (!(type.flags & TypeFlags.Union)) {
// If the active non-union type would be removed from a union by this type guard, return an empty union
return filterUnion(type) ? type : emptyUnionType;
}
return getUnionType(filter((type as UnionType).types, filterUnion), /*noSubtypeReduction*/ true);
function filterUnion(type: Type) {
return assumeTrue === !!(type.flags & flags);
}
}
@@ -6507,7 +6504,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)
]);
}
}
@@ -6518,7 +6515,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 {
@@ -12736,9 +12733,14 @@ namespace ts {
// After we remove all types that are StringLike, we will know if there was a string constituent
// based on whether the remaining type is the same as the initial type.
const arrayType = removeTypesFromUnionType(arrayOrStringType, TypeFlags.StringLike, /*isTypeOfKind*/ true, /*allowEmptyUnionResult*/ true);
let arrayType = arrayOrStringType;
if (arrayOrStringType.flags & TypeFlags.Union) {
arrayType = getUnionType(filter((arrayOrStringType as UnionType).types, t => !(t.flags & TypeFlags.StringLike)));
}
else if (arrayOrStringType.flags & TypeFlags.StringLike) {
arrayType = emptyUnionType;
}
const hasStringConstituent = arrayOrStringType !== arrayType;
let reportedError = false;
if (hasStringConstituent) {
if (languageVersion < ScriptTarget.ES5) {