From b68c37cfebaeae1069936712fa6071f7362d6fef Mon Sep 17 00:00:00 2001 From: Sam Verschueren Date: Thu, 17 Oct 2019 07:47:49 +0200 Subject: [PATCH] Check if types are identical in strict assertions - fixes #43 --- libraries/typescript/lib/typescript.d.ts | 27 ++++++++++++++----- libraries/typescript/lib/typescript.js | 1 + readme.md | 2 +- source/lib/assertions/assert.ts | 4 +-- .../assertions/handlers/strict-assertion.ts | 8 +++++- .../strict-types/loose/index.test-d.ts | 6 +++++ source/test/test.ts | 4 ++- 7 files changed, 40 insertions(+), 12 deletions(-) diff --git a/libraries/typescript/lib/typescript.d.ts b/libraries/typescript/lib/typescript.d.ts index 0a900af9..82b1fc05 100644 --- a/libraries/typescript/lib/typescript.d.ts +++ b/libraries/typescript/lib/typescript.d.ts @@ -1,14 +1,14 @@ /*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. +Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - +License at http://www.apache.org/licenses/LICENSE-2.0 + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABLITY OR NON-INFRINGEMENT. - +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ @@ -1995,6 +1995,19 @@ declare namespace ts { getAugmentedPropertiesOfType(type: Type): Symbol[]; getRootSymbols(symbol: Symbol): ReadonlyArray; getContextualType(node: Expression): Type | undefined; + /** + * Two types are considered identical when + * - they are both the `any` type, + * - they are the same primitive type, + * - they are the same type parameter, + * - they are union types with identical sets of constituent types, or + * - they are intersection types with identical sets of constituent types, or + * - they are object types with identical sets of members. + * + * This relationship is bidirectional. + * See [here](https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3.11.2) for more information. + */ + isIdenticalTo(a: Type, b: Type): boolean; /** * Checks if type `a` is assignable to type `b`. */ @@ -5803,4 +5816,4 @@ declare namespace ts { function transform(source: T | T[], transformers: TransformerFactory[], compilerOptions?: CompilerOptions): TransformationResult; } -export = ts; \ No newline at end of file +export = ts; diff --git a/libraries/typescript/lib/typescript.js b/libraries/typescript/lib/typescript.js index fc75c60f..6287c301 100644 --- a/libraries/typescript/lib/typescript.js +++ b/libraries/typescript/lib/typescript.js @@ -32344,6 +32344,7 @@ var ts; var parsed = ts.getParseTreeNode(node, ts.isFunctionLike); return parsed ? isImplementationOfOverload(parsed) : undefined; }, + isIdenticalTo: isTypeIdenticalTo, isAssignableTo: isTypeAssignableTo, getImmediateAliasedSymbol: getImmediateAliasedSymbol, getAliasedSymbol: resolveAlias, diff --git a/readme.md b/readme.md index 68930022..431e049e 100644 --- a/readme.md +++ b/readme.md @@ -142,7 +142,7 @@ These options will be overridden if a `tsconfig.json` file is found in your proj ### expectType<T>(value) -Check if a value is of a specific type. +Check that `value` is identical to type `T`. ### expectError(function) diff --git a/source/lib/assertions/assert.ts b/source/lib/assertions/assert.ts index d295403a..522d3dd9 100644 --- a/source/lib/assertions/assert.ts +++ b/source/lib/assertions/assert.ts @@ -1,7 +1,7 @@ /** - * Assert the type of the value. + * Check that `value` is identical to type `T`. * - * @param value - Value that should be type checked. + * @param value - Value that should be identical to type `T`. */ // @ts-ignore export const expectType = (value: T) => { // tslint:disable-line:no-unused diff --git a/source/lib/assertions/handlers/strict-assertion.ts b/source/lib/assertions/handlers/strict-assertion.ts index 676dd348..a97708d9 100644 --- a/source/lib/assertions/handlers/strict-assertion.ts +++ b/source/lib/assertions/handlers/strict-assertion.ts @@ -31,12 +31,18 @@ export const strictAssertion = (checker: TypeChecker, nodes: Set continue; } - if (!checker.isAssignableTo(expectedType, argumentType)) { // tslint:disable-line:early-exit + if (!checker.isAssignableTo(expectedType, argumentType)) { /** * The expected type is not assignable to the argument type, but the argument type is * assignable to the expected type. This means our type is too wide. */ diagnostics.push(makeDiagnostic(node, `Parameter type \`${checker.typeToString(expectedType)}\` is declared too wide for argument type \`${checker.typeToString(argumentType)}\`.`)); + } else if (!checker.isIdenticalTo(expectedType, argumentType)) { + /** + * The expected type and argument type are assignable in both directions. We still have to check + * if the types are identical. See https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3.11.2. + */ + diagnostics.push(makeDiagnostic(node, `Parameter type \`${checker.typeToString(expectedType)}\` is not identical to argument type \`${checker.typeToString(argumentType)}\`.`)); } } diff --git a/source/test/fixtures/strict-types/loose/index.test-d.ts b/source/test/fixtures/strict-types/loose/index.test-d.ts index b70f52b4..a5472a0a 100644 --- a/source/test/fixtures/strict-types/loose/index.test-d.ts +++ b/source/test/fixtures/strict-types/loose/index.test-d.ts @@ -28,3 +28,9 @@ abstract class Foo { expectType> | Foo | Foo>( one | Foo | Foo | string>>() ); + +expectType(one()); + +expectType | Observable | Observable>( + one | Observable | Observable>() +); diff --git a/source/test/test.ts b/source/test/test.ts index b69a398d..487f4cb6 100644 --- a/source/test/test.ts +++ b/source/test/test.ts @@ -237,7 +237,9 @@ test('loose types', async t => { [14, 0, 'error', 'Parameter type `Promise` is declared too wide for argument type `Promise`.'], [16, 0, 'error', 'Parameter type `Observable` is declared too wide for argument type `Observable`.'], [20, 0, 'error', 'Parameter type `Observable | Observable` is declared too wide for argument type `Observable | Observable`.'], - [28, 0, 'error', 'Parameter type `Foo> | Foo | Foo` is declared too wide for argument type `Foo | Foo | Foo>`.'] + [28, 0, 'error', 'Parameter type `Foo> | Foo | Foo` is declared too wide for argument type `Foo | Foo | Foo>`.'], + [32, 0, 'error', 'Parameter type `string | number` is not identical to argument type `any`.'], + [34, 0, 'error', 'Parameter type `Observable | Observable | Observable` is not identical to argument type `Observable | Observable | Observable`.'] ]); });