From 4a8f94a5535548a3b9cc8eaf4c02c0830fba088f Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 10 Jun 2016 16:17:32 -0700 Subject: [PATCH] Type guards using discriminant properties of string literal types --- src/compiler/binder.ts | 56 ++++++++++++++++++++++++++++++----------- src/compiler/checker.ts | 47 ++++++++++++++++++++++++++++------ 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 8c48d984b78..f9d0f443347 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -577,12 +577,6 @@ namespace ts { } } - function isNarrowableReference(expr: Expression): boolean { - return expr.kind === SyntaxKind.Identifier || - expr.kind === SyntaxKind.ThisKeyword || - expr.kind === SyntaxKind.PropertyAccessExpression && isNarrowableReference((expr).expression); - } - function isNarrowingExpression(expr: Expression): boolean { switch (expr.kind) { case SyntaxKind.Identifier: @@ -590,7 +584,7 @@ namespace ts { case SyntaxKind.PropertyAccessExpression: return isNarrowableReference(expr); case SyntaxKind.CallExpression: - return true; + return hasNarrowableArgument(expr); case SyntaxKind.ParenthesizedExpression: return isNarrowingExpression((expr).expression); case SyntaxKind.BinaryExpression: @@ -601,6 +595,27 @@ namespace ts { return false; } + function isNarrowableReference(expr: Expression): boolean { + return expr.kind === SyntaxKind.Identifier || + expr.kind === SyntaxKind.ThisKeyword || + expr.kind === SyntaxKind.PropertyAccessExpression && isNarrowableReference((expr).expression); + } + + function hasNarrowableArgument(expr: CallExpression) { + if (expr.arguments) { + for (const argument of expr.arguments) { + if (isNarrowableReference(argument)) { + return true; + } + } + } + if (expr.expression.kind === SyntaxKind.PropertyAccessExpression && + isNarrowableReference((expr.expression).expression)) { + return true; + } + return false; + } + function isNarrowingBinaryExpression(expr: BinaryExpression) { switch (expr.operatorToken.kind) { case SyntaxKind.EqualsToken: @@ -609,21 +624,32 @@ namespace ts { case SyntaxKind.ExclamationEqualsToken: case SyntaxKind.EqualsEqualsEqualsToken: case SyntaxKind.ExclamationEqualsEqualsToken: - if (isNarrowingExpression(expr.left) && (expr.right.kind === SyntaxKind.NullKeyword || expr.right.kind === SyntaxKind.Identifier)) { - return true; - } - if (expr.left.kind === SyntaxKind.TypeOfExpression && isNarrowingExpression((expr.left).expression) && expr.right.kind === SyntaxKind.StringLiteral) { - return true; - } - return false; + return (expr.right.kind === SyntaxKind.NullKeyword || expr.right.kind === SyntaxKind.Identifier && (expr.right).text === "undefined") && isNarrowableOperand(expr.left) || + expr.left.kind === SyntaxKind.PropertyAccessExpression && isNarrowableReference((expr.left).expression) || + expr.left.kind === SyntaxKind.TypeOfExpression && isNarrowableOperand((expr.left).expression) && expr.right.kind === SyntaxKind.StringLiteral; case SyntaxKind.InstanceOfKeyword: - return isNarrowingExpression(expr.left); + return isNarrowableOperand(expr.left); case SyntaxKind.CommaToken: return isNarrowingExpression(expr.right); } return false; } + function isNarrowableOperand(expr: Expression): boolean { + switch (expr.kind) { + case SyntaxKind.ParenthesizedExpression: + return isNarrowableOperand((expr).expression); + case SyntaxKind.BinaryExpression: + switch ((expr).operatorToken.kind) { + case SyntaxKind.EqualsToken: + return isNarrowableOperand((expr).left); + case SyntaxKind.CommaToken: + return isNarrowableOperand((expr).right); + } + } + return isNarrowableReference(expr); + } + function createBranchLabel(): FlowLabel { return { flags: FlowFlags.BranchLabel, diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 65335117cf2..105401c0015 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -5160,7 +5160,6 @@ namespace ts { if (hasProperty(stringLiteralTypes, text)) { return stringLiteralTypes[text]; } - const type = stringLiteralTypes[text] = createType(TypeFlags.StringLiteral); type.text = text; return type; @@ -5625,6 +5624,10 @@ namespace ts { return checkTypeComparableTo(source, target, /*errorNode*/ undefined); } + function areTypesComparable(type1: Type, type2: Type): boolean { + return isTypeComparableTo(type1, type2) || isTypeComparableTo(type2, type1); + } + function checkTypeSubtypeOf(source: Type, target: Type, errorNode: Node, headMessage?: DiagnosticMessage, containingMessageChain?: DiagnosticMessageChain): boolean { return checkTypeRelatedTo(source, target, subtypeRelation, errorNode, headMessage, containingMessageChain); } @@ -6805,8 +6808,10 @@ namespace ts { return !!getPropertyOfType(type, "0"); } - function isStringLiteralType(type: Type) { - return type.flags & TypeFlags.StringLiteral; + function isStringLiteralUnionType(type: Type): boolean { + return type.flags & TypeFlags.StringLiteral ? true : + type.flags & TypeFlags.Union ? forEach((type).types, isStringLiteralUnionType) : + false; } /** @@ -7873,6 +7878,9 @@ namespace ts { if (isNullOrUndefinedLiteral(expr.right)) { return narrowTypeByNullCheck(type, expr, assumeTrue); } + if (expr.left.kind === SyntaxKind.PropertyAccessExpression) { + return narrowTypeByDiscriminant(type, expr, assumeTrue); + } if (expr.left.kind === SyntaxKind.TypeOfExpression && expr.right.kind === SyntaxKind.StringLiteral) { return narrowTypeByTypeof(type, expr, assumeTrue); } @@ -7903,6 +7911,33 @@ namespace ts { return getTypeWithFacts(type, facts); } + function narrowTypeByDiscriminant(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { + // We have '==', '!=', '===', or '!==' operator with property access on left + if (!(type.flags & TypeFlags.Union) || !isMatchingReference(reference, (expr.left).expression)) { + return type; + } + const propName = (expr.left).name.text; + const propType = getTypeOfPropertyOfType(type, propName); + if (!propType || !isStringLiteralUnionType(propType)) { + return type; + } + const discriminantType = expr.right.kind === SyntaxKind.StringLiteral ? getStringLiteralTypeForText((expr.right).text) : checkExpression(expr.right); + if (!isStringLiteralUnionType(discriminantType)) { + return type; + } + if (expr.operatorToken.kind === SyntaxKind.ExclamationEqualsToken || + expr.operatorToken.kind === SyntaxKind.ExclamationEqualsEqualsToken) { + assumeTrue = !assumeTrue; + } + if (assumeTrue) { + return getUnionType(filter((type).types, t => areTypesComparable(getTypeOfPropertyOfType(t, propName), discriminantType))); + } + if (discriminantType.flags & TypeFlags.StringLiteral) { + return getUnionType(filter((type).types, t => getTypeOfPropertyOfType(t, propName) !== discriminantType)); + } + return type; + } + function narrowTypeByTypeof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { // We have '==', '!=', '====', or !==' operator with 'typeof xxx' on the left // and string literal on the right @@ -8892,10 +8927,6 @@ namespace ts { return applyToContextualType(type, t => getIndexTypeOfStructuredType(t, kind)); } - function contextualTypeIsStringLiteralType(type: Type): boolean { - return !!(type.flags & TypeFlags.Union ? forEach((type).types, isStringLiteralType) : isStringLiteralType(type)); - } - // Return true if the given contextual type is a tuple-like type function contextualTypeIsTupleLikeType(type: Type): boolean { return !!(type.flags & TypeFlags.Union ? forEach((type).types, isTupleLikeType) : isTupleLikeType(type)); @@ -12557,7 +12588,7 @@ namespace ts { function checkStringLiteralExpression(node: StringLiteral): Type { const contextualType = getContextualType(node); - if (contextualType && contextualTypeIsStringLiteralType(contextualType)) { + if (contextualType && isStringLiteralUnionType(contextualType)) { return getStringLiteralTypeForText(node.text); }