Skip to content

Commit d70494f

Browse files
committedOct 7, 2014
Narrowing of variable types using typeof/instanceof type guards
1 parent e836fe1 commit d70494f

File tree

2 files changed

+277
-35
lines changed

2 files changed

+277
-35
lines changed
 

‎src/compiler/checker.ts

+267-26
Original file line numberDiff line numberDiff line change
@@ -3844,44 +3844,285 @@ module ts {
38443844

38453845
// EXPRESSION TYPE CHECKING
38463846

3847-
function checkIdentifier(node: Identifier): Type {
3848-
function isInTypeQuery(node: Node): boolean {
3849-
// TypeScript 1.0 spec (April 2014): 3.6.3
3850-
// A type query consists of the keyword typeof followed by an expression.
3851-
// The expression is restricted to a single identifier or a sequence of identifiers separated by periods
3852-
while (node) {
3847+
function getResolvedSymbol(node: Identifier): Symbol {
3848+
var links = getNodeLinks(node);
3849+
if (!links.resolvedSymbol) {
3850+
links.resolvedSymbol = resolveName(node, node.text, SymbolFlags.Value | SymbolFlags.ExportValue, Diagnostics.Cannot_find_name_0, identifierToString(node)) || unknownSymbol;
3851+
}
3852+
return links.resolvedSymbol;
3853+
}
3854+
3855+
function isInTypeQuery(node: Node): boolean {
3856+
// TypeScript 1.0 spec (April 2014): 3.6.3
3857+
// A type query consists of the keyword typeof followed by an expression.
3858+
// The expression is restricted to a single identifier or a sequence of identifiers separated by periods
3859+
while (node) {
3860+
switch (node.kind) {
3861+
case SyntaxKind.TypeQuery:
3862+
return true;
3863+
case SyntaxKind.Identifier:
3864+
case SyntaxKind.QualifiedName:
3865+
node = node.parent;
3866+
continue;
3867+
default:
3868+
return false;
3869+
}
3870+
}
3871+
Debug.fail("should not get here");
3872+
}
3873+
3874+
// Remove one or more primitive types from a union type
3875+
function subtractPrimitiveTypes(type: Type, subtractMask: TypeFlags): Type {
3876+
if (type.flags & TypeFlags.Union) {
3877+
var types = (<UnionType>type).types;
3878+
if (forEach(types, t => t.flags & subtractMask)) {
3879+
var newTypes: Type[] = [];
3880+
forEach(types, t => {
3881+
if (!(t.flags & subtractMask)) {
3882+
newTypes.push(t);
3883+
}
3884+
});
Has conversations. Original line has conversations.
3885+
return getUnionType(newTypes);
3886+
}
3887+
}
3888+
return type;
3889+
}
3890+
3891+
// Check if a given variable is assigned within a given syntax node
3892+
function IsVariableAssignedWithin(symbol: Symbol, node: Node): boolean {
Has a conversation. Original line has a conversation.
3893+
var links = getNodeLinks(node);
3894+
if (links.assignmentChecks) {
3895+
var cachedResult = links.assignmentChecks[symbol.id];
3896+
if (cachedResult !== undefined) {
3897+
return cachedResult;
3898+
}
3899+
}
3900+
else {
3901+
links.assignmentChecks = {};
3902+
}
3903+
return links.assignmentChecks[symbol.id] = isAssignedIn(node);
3904+
3905+
function isAssignedInBinaryExpression(node: BinaryExpression) {
3906+
if (node.operator >= SyntaxKind.FirstAssignment && node.operator <= SyntaxKind.LastAssignment) {
3907+
var n = node.left;
3908+
while (n.kind === SyntaxKind.ParenExpression) {
3909+
n = (<ParenExpression>n).expression;
3910+
}
3911+
if (n.kind === SyntaxKind.Identifier && getResolvedSymbol(<Identifier>n) === symbol) {
3912+
return true;
3913+
}
3914+
}
3915+
return forEachChild(node, isAssignedIn);
3916+
}
3917+
3918+
function isAssignedInVariableDeclaration(node: VariableDeclaration) {
3919+
if (getSymbolOfNode(node) === symbol && node.initializer) {
3920+
return true;
3921+
}
3922+
return forEachChild(node, isAssignedIn);
3923+
}
3924+
3925+
function isAssignedIn(node: Node): boolean {
3926+
switch (node.kind) {
3927+
case SyntaxKind.BinaryExpression:
3928+
return isAssignedInBinaryExpression(<BinaryExpression>node);
3929+
case SyntaxKind.VariableDeclaration:
3930+
return isAssignedInVariableDeclaration(<VariableDeclaration>node);
3931+
case SyntaxKind.ArrayLiteral:
3932+
case SyntaxKind.ObjectLiteral:
3933+
case SyntaxKind.PropertyAccess:
3934+
case SyntaxKind.IndexedAccess:
3935+
case SyntaxKind.CallExpression:
3936+
case SyntaxKind.NewExpression:
3937+
case SyntaxKind.TypeAssertion:
3938+
case SyntaxKind.ParenExpression:
3939+
case SyntaxKind.PrefixOperator:
3940+
case SyntaxKind.PostfixOperator:
3941+
case SyntaxKind.ConditionalExpression:
3942+
case SyntaxKind.Block:
3943+
case SyntaxKind.VariableStatement:
3944+
case SyntaxKind.ExpressionStatement:
3945+
case SyntaxKind.IfStatement:
3946+
case SyntaxKind.DoStatement:
3947+
case SyntaxKind.WhileStatement:
3948+
case SyntaxKind.ForStatement:
3949+
case SyntaxKind.ForInStatement:
3950+
case SyntaxKind.ReturnStatement:
3951+
case SyntaxKind.WithStatement:
3952+
case SyntaxKind.SwitchStatement:
3953+
case SyntaxKind.CaseClause:
3954+
case SyntaxKind.DefaultClause:
3955+
case SyntaxKind.LabeledStatement:
3956+
case SyntaxKind.ThrowStatement:
3957+
case SyntaxKind.TryStatement:
3958+
case SyntaxKind.TryBlock:
3959+
case SyntaxKind.CatchBlock:
3960+
case SyntaxKind.FinallyBlock:
Has conversations. Original line has conversations.
3961+
return forEachChild(node, isAssignedIn);
3962+
}
3963+
return false;
3964+
}
3965+
}
3966+
3967+
// Get the narrowed type of a given symbol at a given location
3968+
function getNarrowedTypeOfSymbol(symbol: Symbol, node: Node) {
Has conversations. Original line has conversations.
3969+
var type = getTypeOfSymbol(symbol);
3970+
// Only narrow when symbol is variable of a non-primitive type
3971+
if (symbol.flags & SymbolFlags.Variable && isTypeAnyOrObjectOrTypeParameter(type)) {
3972+
while (true) {
3973+
var child = node;
3974+
node = node.parent;
3975+
// Stop at containing function or module block
3976+
if (!node || node.kind === SyntaxKind.FunctionBlock || node.kind === SyntaxKind.ModuleBlock) {
Has conversations. Original line has conversations.
3977+
break;
3978+
}
3979+
var narrowedType = type;
38533980
switch (node.kind) {
3854-
case SyntaxKind.TypeQuery:
3855-
return true;
3856-
case SyntaxKind.Identifier:
3857-
case SyntaxKind.QualifiedName:
3858-
node = node.parent;
3859-
continue;
3860-
default:
3861-
return false;
3981+
case SyntaxKind.IfStatement:
3982+
// In a branch of an if statement, narrow based on controlling expression
3983+
if (child !== (<IfStatement>node).expression) {
3984+
narrowedType = narrowType(type, (<IfStatement>node).expression, child === (<IfStatement>node).thenStatement);
Has a conversation. Original line has a conversation.
3985+
}
3986+
break;
3987+
case SyntaxKind.ConditionalExpression:
3988+
// In a branch of a conditional expression, narrow based on controlling condition
3989+
if (child !== (<ConditionalExpression>node).condition) {
3990+
narrowedType = narrowType(type, (<ConditionalExpression>node).condition, child === (<ConditionalExpression>node).whenTrue);
3991+
}
3992+
break;
3993+
case SyntaxKind.BinaryExpression:
3994+
// In the right operand of an && or ||, narrow based on left operand
3995+
if (child === (<BinaryExpression>node).right) {
3996+
if ((<BinaryExpression>node).operator === SyntaxKind.AmpersandAmpersandToken) {
3997+
narrowedType = narrowType(type, (<BinaryExpression>node).left, true);
3998+
}
3999+
else if ((<BinaryExpression>node).operator === SyntaxKind.BarBarToken) {
4000+
narrowedType = narrowType(type, (<BinaryExpression>node).left, false);
4001+
}
4002+
}
4003+
break;
4004+
}
4005+
// Only use narrowed type if construct contains no assignments to variable
4006+
if (narrowedType !== type && !IsVariableAssignedWithin(symbol, node)) {
Has conversations. Original line has conversations.
4007+
type = narrowedType;
38624008
}
38634009
}
3864-
Debug.fail("should not get here");
4010+
}
4011+
return type;
4012+
4013+
function narrowTypeByEquality(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
4014+
var left = <UnaryExpression>expr.left;
4015+
var right = <LiteralExpression>expr.right;
4016+
// Check that we have 'typeof <symbol>' on the left and string literal on the right
Has conversations. Original line has conversations.
4017+
if (left.kind !== SyntaxKind.PrefixOperator || left.operator !== SyntaxKind.TypeOfKeyword ||
4018+
left.operand.kind !== SyntaxKind.Identifier || right.kind !== SyntaxKind.StringLiteral ||
4019+
getResolvedSymbol(<Identifier>left.operand) !== symbol) {
4020+
return type;
4021+
}
4022+
var t = right.text;
4023+
var checkType: Type = t === "string" ? stringType : t === "number" ? numberType : t === "boolean" ? booleanType : emptyObjectType;
4024+
if (expr.operator === SyntaxKind.ExclamationEqualsEqualsToken) {
4025+
assumeTrue = !assumeTrue;
4026+
}
4027+
if (assumeTrue) {
4028+
// The assumed result is true. If check was for a primitive type, that type is the narrowed type. Otherwise we can
4029+
// remove the primitive types from the narrowed type.
4030+
return checkType === emptyObjectType ? subtractPrimitiveTypes(type, TypeFlags.String | TypeFlags.Number | TypeFlags.Boolean) : checkType;
4031+
}
4032+
else {
4033+
// The assumed result is false. If check was for a primitive type we can remove that type from the narrowed type.
4034+
// Otherwise we don't have enough information to do anything.
4035+
return checkType === emptyObjectType ? type : subtractPrimitiveTypes(type, checkType.flags);
4036+
}
38654037
}
38664038

3867-
var symbol = resolveName(node, node.text, SymbolFlags.Value | SymbolFlags.ExportValue, Diagnostics.Cannot_find_name_0, identifierToString(node));
3868-
if (!symbol) {
3869-
symbol = unknownSymbol;
4039+
function narrowTypeByAnd(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
4040+
if (assumeTrue) {
4041+
// The assumed result is true, therefore we narrow assuming each operand to be true.
4042+
return narrowType(narrowType(type, expr.left, true), expr.right, true);
4043+
}
4044+
else {
4045+
// The assumed result is true. This means either the first operand was false, or the first operand was true
Has conversations. Original line has conversations.
4046+
// and the second operand was false. We narrow with those assumptions and union the two resulting types.
4047+
return getUnionType([narrowType(type, expr.left, false), narrowType(narrowType(type, expr.left, true), expr.right, false)]);
4048+
}
38704049
}
38714050

4051+
function narrowTypeByOr(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
4052+
if (assumeTrue) {
4053+
// The assumed result is true. This means either the first operand was true, or the first operand was false
4054+
// and the second operand was true. We narrow with those assumptions and union the two resulting types.
4055+
return getUnionType([narrowType(type, expr.left, true), narrowType(narrowType(type, expr.left, false), expr.right, true)]);
4056+
}
4057+
else {
4058+
// The assumed result is false, therefore we narrow assuming each operand to be false.
4059+
return narrowType(narrowType(type, expr.left, false), expr.right, false);
4060+
}
4061+
}
4062+
4063+
function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
Has conversations. Original line has conversations.
4064+
// Check that we have variable symbol on the left
4065+
if (expr.left.kind !== SyntaxKind.Identifier || getResolvedSymbol(<Identifier>expr.left) !== symbol) {
4066+
return type;
4067+
}
4068+
// Check that right operand is a function type with a prototype property
4069+
var rightType = checkExpression(expr.right);
4070+
if (!isTypeSubtypeOf(rightType, globalFunctionType)) {
4071+
return type;
4072+
}
4073+
var prototypeProperty = getPropertyOfType(getApparentType(rightType), "prototype");
Has conversations. Original line has conversations.
4074+
if (!prototypeProperty) {
4075+
return type;
4076+
}
4077+
var prototypeType = getTypeOfSymbol(prototypeProperty);
4078+
// Narrow to type of prototype property if it is a subtype of current type
4079+
return isTypeSubtypeOf(prototypeType, type) ? prototypeType : type;
Has conversations. Original line has conversations.
4080+
}
4081+
4082+
// Narrow the given type based on the given expression having the assumed boolean value
4083+
function narrowType(type: Type, expr: Expression, assumeTrue: boolean): Type {
4084+
switch (expr.kind) {
4085+
case SyntaxKind.ParenExpression:
4086+
return narrowType(type, (<ParenExpression>expr).expression, assumeTrue);
4087+
case SyntaxKind.BinaryExpression:
4088+
var operator = (<BinaryExpression>expr).operator;
4089+
if (operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) {
4090+
return narrowTypeByEquality(type, <BinaryExpression>expr, assumeTrue);
4091+
}
4092+
else if (operator === SyntaxKind.AmpersandAmpersandToken) {
4093+
return narrowTypeByAnd(type, <BinaryExpression>expr, assumeTrue);
4094+
}
4095+
else if (operator === SyntaxKind.BarBarToken) {
4096+
return narrowTypeByOr(type, <BinaryExpression>expr, assumeTrue);
4097+
}
4098+
else if (operator === SyntaxKind.InstanceOfKeyword) {
4099+
return narrowTypeByInstanceof(type, <BinaryExpression>expr, assumeTrue);
4100+
}
4101+
break;
4102+
case SyntaxKind.PrefixOperator:
4103+
if ((<UnaryExpression>expr).operator === SyntaxKind.ExclamationToken) {
4104+
return narrowType(type, (<UnaryExpression>expr).operand, !assumeTrue);
4105+
}
4106+
break;
4107+
}
4108+
return type;
4109+
}
4110+
}
4111+
4112+
function checkIdentifier(node: Identifier): Type {
4113+
var symbol = getResolvedSymbol(node);
4114+
38724115
if (symbol.flags & SymbolFlags.Import) {
38734116
// Mark the import as referenced so that we emit it in the final .js file.
38744117
// exception: identifiers that appear in type queries
38754118
getSymbolLinks(symbol).referenced = !isInTypeQuery(node);
38764119
}
38774120

3878-
getNodeLinks(node).resolvedSymbol = symbol;
3879-
38804121
checkCollisionWithCapturedSuperVariable(node, node);
38814122
checkCollisionWithCapturedThisVariable(node, node);
38824123
checkCollisionWithIndexVariableInGeneratedCode(node, node);
38834124

3884-
return getTypeOfSymbol(getExportSymbolOfValueSymbolIfExported(symbol));
4125+
return getNarrowedTypeOfSymbol(getExportSymbolOfValueSymbolIfExported(symbol), node);
38854126
}
38864127

38874128
function captureLexicalThis(node: Node, container: Node): void {
@@ -5134,8 +5375,8 @@ module ts {
51345375
return numberType;
51355376
}
51365377

5137-
function isTypeAnyTypeObjectTypeOrTypeParameter(type: Type): boolean {
5138-
return type === anyType || ((type.flags & (TypeFlags.ObjectType | TypeFlags.TypeParameter)) !== 0);
5378+
function isTypeAnyOrObjectOrTypeParameter(type: Type): boolean {
5379+
return (type.flags & (TypeFlags.Any | TypeFlags.ObjectType | TypeFlags.TypeParameter)) !== 0;
51395380
}
51405381

51415382
function checkInstanceOfExpression(node: BinaryExpression, leftType: Type, rightType: Type): Type {
@@ -5144,7 +5385,7 @@ module ts {
51445385
// and the right operand to be of type Any or a subtype of the 'Function' interface type.
51455386
// The result is always of the Boolean primitive type.
51465387
// NOTE: do not raise error if leftType is unknown as related error was already reported
5147-
if (leftType !== unknownType && !isTypeAnyTypeObjectTypeOrTypeParameter(leftType)) {
5388+
if (leftType !== unknownType && !isTypeAnyOrObjectOrTypeParameter(leftType)) {
51485389
error(node.left, Diagnostics.The_left_hand_side_of_an_instanceof_expression_must_be_of_type_any_an_object_type_or_a_type_parameter);
51495390
}
51505391
// NOTE: do not raise error if right is unknown as related error was already reported
@@ -5162,7 +5403,7 @@ module ts {
51625403
if (leftType !== anyType && leftType !== stringType && leftType !== numberType) {
51635404
error(node.left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_of_types_any_string_or_number);
51645405
}
5165-
if (!isTypeAnyTypeObjectTypeOrTypeParameter(rightType)) {
5406+
if (!isTypeAnyOrObjectOrTypeParameter(rightType)) {
51665407
error(node.right, Diagnostics.The_right_hand_side_of_an_in_expression_must_be_of_type_any_an_object_type_or_a_type_parameter);
51675408
}
51685409
return booleanType;
@@ -6338,7 +6579,7 @@ module ts {
63386579
var exprType = checkExpression(node.expression);
63396580
// unknownType is returned i.e. if node.expression is identifier whose name cannot be resolved
63406581
// in this case error about missing name is already reported - do not report extra one
6341-
if (!isTypeAnyTypeObjectTypeOrTypeParameter(exprType) && exprType !== unknownType) {
6582+
if (!isTypeAnyOrObjectOrTypeParameter(exprType) && exprType !== unknownType) {
63426583
error(node.expression, Diagnostics.The_right_hand_side_of_a_for_in_statement_must_be_of_type_any_an_object_type_or_a_type_parameter);
63436584
}
63446585

1 commit comments

Comments
 (1)

JsonFreeman commented on Oct 8, 2014

@JsonFreeman
Contributor

Just want to say overall that this idea is really awesome!

Please sign in to comment.