diff --git a/readme.md b/readme.md
index 431e049e..aaf0aa3e 100644
--- a/readme.md
+++ b/readme.md
@@ -77,6 +77,16 @@ If we run `tsd`, we will notice that it reports an error because the `concat` me
+If you still want loose type assertion, you can use `expectAssignable` for that.
+
+```ts
+import {expectType, expectAssignable} from 'tsd';
+import concat from '.';
+
+expectType(concat('foo', 'bar'));
+expectAssignable(concat('foo', 'bar'));
+```
+
### Top-level `await`
If your method returns a `Promise`, you can use top-level `await` to resolve the value instead of wrapping it in an `async` [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE).
@@ -142,7 +152,15 @@ These options will be overridden if a `tsconfig.json` file is found in your proj
### expectType<T>(value)
-Check that `value` is identical to type `T`.
+Check that the type of `value` is identical to type `T`.
+
+### expectAssignable<T>(value)
+
+Check that the type of `value` is assignable to type `T`.
+
+### expectNotAssignable<T>(value)
+
+Check that the type of `value` is not assignable to type `T`.
### expectError(function)
diff --git a/source/lib/assertions/assert.ts b/source/lib/assertions/assert.ts
index 522d3dd9..95442fdb 100644
--- a/source/lib/assertions/assert.ts
+++ b/source/lib/assertions/assert.ts
@@ -1,5 +1,5 @@
/**
- * Check that `value` is identical to type `T`.
+ * Check that the type of `value` is identical to type `T`.
*
* @param value - Value that should be identical to type `T`.
*/
@@ -8,6 +8,26 @@ export const expectType = (value: T) => { // tslint:disable-line:no-unused
// Do nothing, the TypeScript compiler handles this for us
};
+/**
+ * Check that the type of `value` is assignable to type `T`.
+ *
+ * @param value - Value that should be assignable to type `T`.
+ */
+// @ts-ignore
+export const expectAssignable = (value: T) => { // tslint:disable-line:no-unused
+ // Do nothing, the TypeScript compiler handles this for us
+};
+
+/**
+ * Check that the type of `value` is not assignable to type `T`.
+ *
+ * @param value - Value that should not be assignable to type `T`.
+ */
+// @ts-ignore
+export const expectNotAssignable = (value: any) => { // tslint:disable-line:no-unused
+ // Do nothing, the TypeScript compiler handles this for us
+};
+
/**
* Assert the value to throw an argument error.
*
diff --git a/source/lib/assertions/handlers/assignability.ts b/source/lib/assertions/handlers/assignability.ts
new file mode 100644
index 00000000..51a5f062
--- /dev/null
+++ b/source/lib/assertions/handlers/assignability.ts
@@ -0,0 +1,39 @@
+import {CallExpression} from '../../../../libraries/typescript/lib/typescript';
+import {TypeChecker} from '../../entities/typescript';
+import {Diagnostic} from '../../interfaces';
+import {makeDiagnostic} from '../../utils';
+
+/**
+ * Verifies that the argument of the assertion is not assignable to the generic type of the assertion.
+ *
+ * @param checker - The TypeScript type checker.
+ * @param nodes - The `expectType` AST nodes.
+ * @return List of custom diagnostics.
+ */
+export const isNotAssignable = (checker: TypeChecker, nodes: Set): Diagnostic[] => {
+ const diagnostics: Diagnostic[] = [];
+
+ if (!nodes) {
+ return diagnostics;
+ }
+
+ for (const node of nodes) {
+ if (!node.typeArguments) {
+ // Skip if the node does not have generics
+ continue;
+ }
+
+ // Retrieve the type to be expected. This is the type inside the generic.
+ const expectedType = checker.getTypeFromTypeNode(node.typeArguments[0]);
+ const argumentType = checker.getTypeAtLocation(node.arguments[0]);
+
+ if (checker.isTypeAssignableTo(argumentType, expectedType)) {
+ /**
+ * The argument type is assignable to the expected type, we don't want this so add a diagnostic.
+ */
+ diagnostics.push(makeDiagnostic(node, `Argument of type \`${checker.typeToString(argumentType)}\` is assignable to parameter of type \`${checker.typeToString(expectedType)}\`.`));
+ }
+ }
+
+ return diagnostics;
+};
diff --git a/source/lib/assertions/handlers/index.ts b/source/lib/assertions/handlers/index.ts
index 090cd101..fc77fc57 100644
--- a/source/lib/assertions/handlers/index.ts
+++ b/source/lib/assertions/handlers/index.ts
@@ -2,3 +2,4 @@ export {Handler} from './handler';
// Handlers
export {strictAssertion} from './strict-assertion';
+export {isNotAssignable} from './assignability';
diff --git a/source/lib/assertions/index.ts b/source/lib/assertions/index.ts
index 5b9862f9..75bce292 100644
--- a/source/lib/assertions/index.ts
+++ b/source/lib/assertions/index.ts
@@ -2,15 +2,19 @@ import {CallExpression} from '../../../libraries/typescript/lib/typescript';
import {TypeChecker} from '../entities/typescript';
import {Diagnostic} from '../interfaces';
import {Handler, strictAssertion} from './handlers';
+import {isNotAssignable} from './handlers/assignability';
export enum Assertion {
EXPECT_TYPE = 'expectType',
- EXPECT_ERROR = 'expectError'
+ EXPECT_ERROR = 'expectError',
+ EXPECT_ASSIGNABLE = 'expectAssignable',
+ EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable'
}
// List of diagnostic handlers attached to the assertion
const assertionHandlers = new Map([
- [Assertion.EXPECT_TYPE, strictAssertion]
+ [Assertion.EXPECT_TYPE, strictAssertion],
+ [Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable]
]);
/**
diff --git a/source/test/assignability.ts b/source/test/assignability.ts
new file mode 100644
index 00000000..ef95556b
--- /dev/null
+++ b/source/test/assignability.ts
@@ -0,0 +1,21 @@
+import * as path from 'path';
+import test from 'ava';
+import {verify} from './fixtures/utils';
+import tsd from '..';
+
+test('assignable', async t => {
+ const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/assignability/assignable')});
+
+ verify(t, diagnostics, [
+ [8, 26, 'error', 'Argument of type \'string\' is not assignable to parameter of type \'boolean\'.']
+ ]);
+});
+
+test('not assignable', async t => {
+ const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/assignability/not-assignable')});
+
+ verify(t, diagnostics, [
+ [4, 0, 'error', 'Argument of type `string` is assignable to parameter of type `string | number`.'],
+ [5, 0, 'error', 'Argument of type `string` is assignable to parameter of type `any`.'],
+ ]);
+});
diff --git a/source/test/fixtures/assignability/assignable/index.d.ts b/source/test/fixtures/assignability/assignable/index.d.ts
new file mode 100644
index 00000000..266914ab
--- /dev/null
+++ b/source/test/fixtures/assignability/assignable/index.d.ts
@@ -0,0 +1,6 @@
+declare const concat: {
+ (foo: string, bar: string): string;
+ (foo: number, bar: number): number;
+};
+
+export default concat;
diff --git a/source/test/fixtures/assignability/assignable/index.js b/source/test/fixtures/assignability/assignable/index.js
new file mode 100644
index 00000000..f17717f5
--- /dev/null
+++ b/source/test/fixtures/assignability/assignable/index.js
@@ -0,0 +1,3 @@
+module.exports.default = (foo, bar) => {
+ return foo + bar;
+};
diff --git a/source/test/fixtures/assignability/assignable/index.test-d.ts b/source/test/fixtures/assignability/assignable/index.test-d.ts
new file mode 100644
index 00000000..d0583acf
--- /dev/null
+++ b/source/test/fixtures/assignability/assignable/index.test-d.ts
@@ -0,0 +1,8 @@
+import {expectAssignable} from '../../../..';
+import concat from '.';
+
+expectAssignable(concat('foo', 'bar'));
+expectAssignable(concat(1, 2));
+expectAssignable(concat(1, 2));
+
+expectAssignable(concat('unicorn', 'rainbow'));
diff --git a/source/test/fixtures/assignability/assignable/package.json b/source/test/fixtures/assignability/assignable/package.json
new file mode 100644
index 00000000..de6dc1db
--- /dev/null
+++ b/source/test/fixtures/assignability/assignable/package.json
@@ -0,0 +1,3 @@
+{
+ "name": "foo"
+}
diff --git a/source/test/fixtures/assignability/not-assignable/index.d.ts b/source/test/fixtures/assignability/not-assignable/index.d.ts
new file mode 100644
index 00000000..266914ab
--- /dev/null
+++ b/source/test/fixtures/assignability/not-assignable/index.d.ts
@@ -0,0 +1,6 @@
+declare const concat: {
+ (foo: string, bar: string): string;
+ (foo: number, bar: number): number;
+};
+
+export default concat;
diff --git a/source/test/fixtures/assignability/not-assignable/index.js b/source/test/fixtures/assignability/not-assignable/index.js
new file mode 100644
index 00000000..f17717f5
--- /dev/null
+++ b/source/test/fixtures/assignability/not-assignable/index.js
@@ -0,0 +1,3 @@
+module.exports.default = (foo, bar) => {
+ return foo + bar;
+};
diff --git a/source/test/fixtures/assignability/not-assignable/index.test-d.ts b/source/test/fixtures/assignability/not-assignable/index.test-d.ts
new file mode 100644
index 00000000..39dfbab2
--- /dev/null
+++ b/source/test/fixtures/assignability/not-assignable/index.test-d.ts
@@ -0,0 +1,8 @@
+import {expectNotAssignable} from '../../../..';
+import concat from '.';
+
+expectNotAssignable(concat('foo', 'bar'));
+expectNotAssignable(concat('foo', 'bar'));
+
+expectNotAssignable(concat('unicorn', 'rainbow'));
+expectNotAssignable(concat('unicorn', 'rainbow'));
diff --git a/source/test/fixtures/assignability/not-assignable/package.json b/source/test/fixtures/assignability/not-assignable/package.json
new file mode 100644
index 00000000..de6dc1db
--- /dev/null
+++ b/source/test/fixtures/assignability/not-assignable/package.json
@@ -0,0 +1,3 @@
+{
+ "name": "foo"
+}
diff --git a/source/test/fixtures/utils.ts b/source/test/fixtures/utils.ts
new file mode 100644
index 00000000..cab9e6e7
--- /dev/null
+++ b/source/test/fixtures/utils.ts
@@ -0,0 +1,30 @@
+import {ExecutionContext} from 'ava';
+import {Diagnostic} from '../../lib/interfaces';
+
+type Expectation = [number, number, 'error' | 'warning', string, (string | RegExp)?];
+
+/**
+ * Verify a list of diagnostics.
+ *
+ * @param t - The AVA execution context.
+ * @param diagnostics - List of diagnostics to verify.
+ * @param expectations - Expected diagnostics.
+ */
+export const verify = (t: ExecutionContext, diagnostics: Diagnostic[], expectations: Expectation[]) => {
+ t.true(diagnostics.length === expectations.length);
+
+ for (const [index, diagnostic] of diagnostics.entries()) {
+ t.is(diagnostic.line, expectations[index][0]);
+ t.is(diagnostic.column, expectations[index][1]);
+ t.is(diagnostic.severity, expectations[index][2]);
+ t.is(diagnostic.message, expectations[index][3]);
+
+ const filename = expectations[index][4];
+
+ if (typeof filename === 'string') {
+ t.is(diagnostic.fileName, filename);
+ } else if (typeof filename === 'object') {
+ t.regex(diagnostic.fileName, filename);
+ }
+ }
+};
diff --git a/source/test/test.ts b/source/test/test.ts
index 487f4cb6..73e19bf3 100644
--- a/source/test/test.ts
+++ b/source/test/test.ts
@@ -1,35 +1,7 @@
import * as path from 'path';
-import test, {ExecutionContext} from 'ava';
+import test from 'ava';
+import {verify} from './fixtures/utils';
import tsd from '..';
-import {Diagnostic} from '../lib/interfaces';
-
-type Expectation = [number, number, 'error' | 'warning', string, (string | RegExp)?];
-
-/**
- * Verify a list of diagnostics.
- *
- * @param t - The AVA execution context.
- * @param diagnostics - List of diagnostics to verify.
- * @param expectations - Expected diagnostics.
- */
-const verify = (t: ExecutionContext, diagnostics: Diagnostic[], expectations: Expectation[]) => {
- t.true(diagnostics.length === expectations.length);
-
- for (const [index, diagnostic] of diagnostics.entries()) {
- t.is(diagnostic.line, expectations[index][0]);
- t.is(diagnostic.column, expectations[index][1]);
- t.is(diagnostic.severity, expectations[index][2]);
- t.is(diagnostic.message, expectations[index][3]);
-
- const filename = expectations[index][4];
-
- if (typeof filename === 'string') {
- t.is(diagnostic.fileName, filename);
- } else if (typeof filename === 'object') {
- t.regex(diagnostic.fileName, filename);
- }
- }
-};
test('throw if no type definition was found', async t => {
await t.throwsAsync(tsd({cwd: path.join(__dirname, 'fixtures/no-tsd')}), 'The type definition `index.d.ts` does not exist. Create one and try again.');