From b7d1df68fb21703e529a2600bf42ad0ce3f6b9be Mon Sep 17 00:00:00 2001 From: Tingan Ho Date: Tue, 2 Jun 2015 23:25:21 +0800 Subject: [PATCH] Adds type guard methods --- src/compiler/checker.ts | 48 ++++-- .../diagnosticInformationMap.generated.ts | 1 + src/compiler/diagnosticMessages.json | 4 + src/compiler/parser.ts | 42 ++++-- .../reference/typeGuardMethods.errors.txt | 73 +++++++++ tests/baselines/reference/typeGuardMethods.js | 141 ++++++++++++++++++ .../typeGuards/typeGuardMethods.ts | 64 ++++++++ 7 files changed, 350 insertions(+), 23 deletions(-) create mode 100644 tests/baselines/reference/typeGuardMethods.errors.txt create mode 100644 tests/baselines/reference/typeGuardMethods.js create mode 100644 tests/cases/conformance/expressions/typeGuards/typeGuardMethods.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a09a2736aac..5a33748fb0b 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -5646,6 +5646,24 @@ module ts { return type; } + function shouldNarrowTypeByTypePredicate(signature: Signature, expr: CallExpression): boolean { + if (!signature.typePredicate) { + return false; + } + if (expr.arguments && + expr.arguments[signature.typePredicate.parameterIndex] && + getSymbolAtLocation(expr.arguments[signature.typePredicate.parameterIndex]) === symbol) { + + return true; + } + if (expr.expression.kind === SyntaxKind.PropertyAccessExpression && + getSymbolAtLocation((expr.expression).expression) === symbol) { + + return true; + } + return false; + } + function narrowTypeByTypePredicate(type: Type, expr: CallExpression, assumeTrue: boolean): Type { if (type.flags & TypeFlags.Any) { return type; @@ -5657,16 +5675,12 @@ module ts { } return type; } - if (signature.typePredicate) { - if (expr.arguments && expr.arguments[signature.typePredicate.parameterIndex]) { - if (getSymbolAtLocation(expr.arguments[signature.typePredicate.parameterIndex]) === symbol) { - if (isTypeSubtypeOf(signature.typePredicate.type, type)) { - return signature.typePredicate.type; - } - if (type.flags & TypeFlags.Union) { - return getUnionType(filter((type).types, t => isTypeSubtypeOf(t, signature.typePredicate.type))); - } - } + if (shouldNarrowTypeByTypePredicate(signature, expr)) { + if (isTypeSubtypeOf(signature.typePredicate.type, type)) { + return signature.typePredicate.type; + } + if (type.flags & TypeFlags.Union) { + return getUnionType(filter((type).types, t => isTypeSubtypeOf(t, signature.typePredicate.type))); } } return type; @@ -8622,8 +8636,7 @@ module ts { links.typeFromTypePredicate = getTypeFromTypeNode(node.typePredicate.type); } if (links.typePredicateParameterIndex >= 0) { - checkTypeAssignableTo( - links.typeFromTypePredicate, + checkTypeAssignableTo(links.typeFromTypePredicate, getTypeAtLocation(node.parameters[links.typePredicateParameterIndex]), node.typePredicate.type); } @@ -8632,6 +8645,17 @@ module ts { Diagnostics.Cannot_find_parameter_0, node.typePredicate.parameterName.text); } + else { + let typeOfClass = getTypeAtLocation(node.parent); + if (!isTypeSubtypeOf(links.typeFromTypePredicate, typeOfClass) && + !isTypeSubtypeOf(typeOfClass, links.typeFromTypePredicate)) { + + error(node.typePredicate, + Diagnostics.Type_0_and_type_1_are_disjoint_types, + typeToString(links.typeFromTypePredicate), + typeToString(typeOfClass)); + } + } } if (produceDiagnostics) { diff --git a/src/compiler/diagnosticInformationMap.generated.ts b/src/compiler/diagnosticInformationMap.generated.ts index 5a87f67ddd3..aba138b9a1b 100644 --- a/src/compiler/diagnosticInformationMap.generated.ts +++ b/src/compiler/diagnosticInformationMap.generated.ts @@ -184,6 +184,7 @@ module ts { Cannot_find_parameter_0: { code: 1226, category: DiagnosticCategory.Error, key: "Cannot find parameter '{0}'." }, Type_guard_annotation_0_is_not_assignable_to_1: { code: 1227, category: DiagnosticCategory.Error, key: "Type-guard annotation '{0}' is not assignable to '{1}'." }, Parameter_index_from_0_does_not_match_the_parameter_index_from_1: { code: 1228, category: DiagnosticCategory.Error, key: "Parameter index from '{0}' does not match the parameter index from '{1}'." }, + Type_0_and_type_1_are_disjoint_types: { code: 1229, category: DiagnosticCategory.Error, key: "Type '{0}' and type '{1}' are disjoint types." }, Duplicate_identifier_0: { code: 2300, category: DiagnosticCategory.Error, key: "Duplicate identifier '{0}'." }, Initializer_of_instance_member_variable_0_cannot_reference_identifier_1_declared_in_the_constructor: { code: 2301, category: DiagnosticCategory.Error, key: "Initializer of instance member variable '{0}' cannot reference identifier '{1}' declared in the constructor." }, Static_members_cannot_reference_class_type_parameters: { code: 2302, category: DiagnosticCategory.Error, key: "Static members cannot reference class type parameters." }, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 0053b071438..a94839fba7d 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -723,6 +723,10 @@ "category": "Error", "code": 1228 }, + "Type '{0}' and type '{1}' are disjoint types.": { + "category": "Error", + "code": 1229 + }, "Duplicate identifier '{0}'.": { diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index ac4517a0b79..0f84ec20383 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -2014,6 +2014,34 @@ module ts { return parseInitializer(/*inParameter*/ true); } + function parseTypePredicate(signature: SignatureDeclaration) { + let node = createNode(SyntaxKind.TypePredicate); + if (token !== SyntaxKind.ThisKeyword) { + node.pos = signature.type.pos; + node.parameterName = (signature.type).typeName; + signature.type = undefined; + } + else { + // Swallow `this` + nextToken(); + } + + // Swallow `is` + nextToken(); + + node.type = parseType(); + signature.typePredicate = finishNode(node); + } + + function parseTypePredicateOrReturnType(signature: SignatureDeclaration) { + if (token === SyntaxKind.ThisKeyword) { + parseTypePredicate(signature); + } + else { + signature.type = parseType(); + } + } + function fillSignature( returnToken: SyntaxKind, yieldAndGeneratorParameterContext: boolean, @@ -2025,21 +2053,13 @@ module ts { if (returnTokenRequired) { parseExpected(returnToken); - signature.type = parseType(); + parseTypePredicateOrReturnType(signature); } else if (parseOptional(returnToken)) { - signature.type = parseType(); + parseTypePredicateOrReturnType(signature); } if (token === SyntaxKind.IsKeyword) { - let node = createNode(SyntaxKind.TypePredicate); - node.pos = signature.type.pos; - node.parameterName = (signature.type).typeName; - - nextToken(); - - node.type = parseType(); - signature.type = undefined; - signature.typePredicate = finishNode(node); + parseTypePredicate(signature); } } diff --git a/tests/baselines/reference/typeGuardMethods.errors.txt b/tests/baselines/reference/typeGuardMethods.errors.txt new file mode 100644 index 00000000000..246f84ccc09 --- /dev/null +++ b/tests/baselines/reference/typeGuardMethods.errors.txt @@ -0,0 +1,73 @@ +tests/cases/conformance/expressions/typeGuards/typeGuardMethods.ts(14,12): error TS1229: Type 'A' and type 'B' are disjoint types. +tests/cases/conformance/expressions/typeGuards/typeGuardMethods.ts(17,12): error TS1229: Type 'C' and type 'B' are disjoint types. + + +==== tests/cases/conformance/expressions/typeGuards/typeGuardMethods.ts (2 errors) ==== + + class A { + propA: number; + isA(): this is A { + return true; + } + isC(): this is C { + return false; + } + } + + class B { + propB: number; + isA(): this is A { + ~~~~~~~~~ +!!! error TS1229: Type 'A' and type 'B' are disjoint types. + return false; + } + isC(): this is C { + ~~~~~~~~~ +!!! error TS1229: Type 'C' and type 'B' are disjoint types. + return false; + } + } + + class C extends A { + propC: number; + isA(): this is A { + return false; + } + isC(): this is C { + return true; + } + } + + class D extends C { + isA(): this is A { + return false; + } + isString(x: any): x is string { // with parameter declaration + return true; + } + } + + var a: A; + + // Basic. + if (a.isC()) { + a.propC; + } + + // Sub type. + var subType: C; + if(subType.isA()) { + subType.propC; + } + + // Union type. + var union: A | B; + if(union.isA()) { + union.propA; + } + + var b: any; + var d = new D; + if(d.isString(b)) { + b.length; + } \ No newline at end of file diff --git a/tests/baselines/reference/typeGuardMethods.js b/tests/baselines/reference/typeGuardMethods.js new file mode 100644 index 00000000000..dcc7eca43c9 --- /dev/null +++ b/tests/baselines/reference/typeGuardMethods.js @@ -0,0 +1,141 @@ +//// [typeGuardMethods.ts] + +class A { + propA: number; + isA(): this is A { + return true; + } + isC(): this is C { + return false; + } +} + +class B { + propB: number; + isA(): this is A { + return false; + } + isC(): this is C { + return false; + } +} + +class C extends A { + propC: number; + isA(): this is A { + return false; + } + isC(): this is C { + return true; + } +} + +class D extends C { + isA(): this is A { + return false; + } + isString(x: any): x is string { // with parameter declaration + return true; + } +} + +var a: A; + +// Basic. +if (a.isC()) { + a.propC; +} + +// Sub type. +var subType: C; +if(subType.isA()) { + subType.propC; +} + +// Union type. +var union: A | B; +if(union.isA()) { + union.propA; +} + +var b: any; +var d = new D; +if(d.isString(b)) { + b.length; +} + +//// [typeGuardMethods.js] +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + __.prototype = b.prototype; + d.prototype = new __(); +}; +var A = (function () { + function A() { + } + A.prototype.isA = function () { + return true; + }; + A.prototype.isC = function () { + return false; + }; + return A; +})(); +var B = (function () { + function B() { + } + B.prototype.isA = function () { + return false; + }; + B.prototype.isC = function () { + return false; + }; + return B; +})(); +var C = (function (_super) { + __extends(C, _super); + function C() { + _super.apply(this, arguments); + } + C.prototype.isA = function () { + return false; + }; + C.prototype.isC = function () { + return true; + }; + return C; +})(A); +var D = (function (_super) { + __extends(D, _super); + function D() { + _super.apply(this, arguments); + } + D.prototype.isA = function () { + return false; + }; + D.prototype.isString = function (x) { + return true; + }; + return D; +})(C); +var a; +// Basic. +if (a.isC()) { + a.propC; +} +// Sub type. +var subType; +if (subType.isA()) { + subType.propC; +} +// Union type. +var union; +if (union.isA()) { + union.propA; +} +var b; +var d = new D; +if (d.isString(b)) { + b.length; +} diff --git a/tests/cases/conformance/expressions/typeGuards/typeGuardMethods.ts b/tests/cases/conformance/expressions/typeGuards/typeGuardMethods.ts new file mode 100644 index 00000000000..80ae65a7ddb --- /dev/null +++ b/tests/cases/conformance/expressions/typeGuards/typeGuardMethods.ts @@ -0,0 +1,64 @@ + +class A { + propA: number; + isA(): this is A { + return true; + } + isC(): this is C { + return false; + } +} + +class B { + propB: number; + isA(): this is A { + return false; + } + isC(): this is C { + return false; + } +} + +class C extends A { + propC: number; + isA(): this is A { + return false; + } + isC(): this is C { + return true; + } +} + +class D extends C { + isA(): this is A { + return false; + } + isString(x: any): x is string { // with parameter declaration + return true; + } +} + +var a: A; + +// Basic. +if (a.isC()) { + a.propC; +} + +// Sub type. +var subType: C; +if(subType.isA()) { + subType.propC; +} + +// Union type. +var union: A | B; +if(union.isA()) { + union.propA; +} + +var b: any; +var d = new D; +if(d.isString(b)) { + b.length; +} \ No newline at end of file