Skip to content

Commit 3a375fa

Browse files
Add assignability expectations - fixes #39
1 parent 206573b commit 3a375fa

File tree

16 files changed

+179
-34
lines changed

16 files changed

+179
-34
lines changed

readme.md

+19-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ If we run `tsd`, we will notice that it reports an error because the `concat` me
7777

7878
<img src="media/strict-assert.png" width="1330">
7979

80+
If you still want loose type assertion, you can use `expectAssignable` for that.
81+
82+
```ts
83+
import {expectType, expectAssignable} from 'tsd';
84+
import concat from '.';
85+
86+
expectType<string>(concat('foo', 'bar'));
87+
expectAssignable<string | number>(concat('foo', 'bar'));
88+
```
89+
8090
### Top-level `await`
8191

8292
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
142152

143153
### expectType&lt;T&gt;(value)
144154

145-
Check that `value` is identical to type `T`.
155+
Check that the type of `value` is identical to type `T`.
156+
157+
### expectAssignable&lt;T&gt;(value)
158+
159+
Check that the type of `value` is assignable to type `T`.
160+
161+
### expectNotAssignable&lt;T&gt;(value)
162+
163+
Check that the type of `value` is not assignable to type `T`.
146164

147165
### expectError(function)
148166

source/lib/assertions/assert.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Check that `value` is identical to type `T`.
2+
* Check that the type of `value` is identical to type `T`.
33
*
44
* @param value - Value that should be identical to type `T`.
55
*/
@@ -8,6 +8,26 @@ export const expectType = <T>(value: T) => { // tslint:disable-line:no-unused
88
// Do nothing, the TypeScript compiler handles this for us
99
};
1010

11+
/**
12+
* Check that the type of `value` is assignable to type `T`.
13+
*
14+
* @param value - Value that should be assignable to type `T`.
15+
*/
16+
// @ts-ignore
17+
export const expectAssignable = <T>(value: T) => { // tslint:disable-line:no-unused
18+
// Do nothing, the TypeScript compiler handles this for us
19+
};
20+
21+
/**
22+
* Check that the type of `value` is not assignable to type `T`.
23+
*
24+
* @param value - Value that should not be assignable to type `T`.
25+
*/
26+
// @ts-ignore
27+
export const expectNotAssignable = <T>(value: any) => { // tslint:disable-line:no-unused
28+
// Do nothing, the TypeScript compiler handles this for us
29+
};
30+
1131
/**
1232
* Assert the value to throw an argument error.
1333
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {CallExpression} from '../../../../libraries/typescript/lib/typescript';
2+
import {TypeChecker} from '../../entities/typescript';
3+
import {Diagnostic} from '../../interfaces';
4+
import {makeDiagnostic} from '../../utils';
5+
6+
/**
7+
* Verifies that the argument of the assertion is not assignable to the generic type of the assertion.
8+
*
9+
* @param checker - The TypeScript type checker.
10+
* @param nodes - The `expectType` AST nodes.
11+
* @return List of custom diagnostics.
12+
*/
13+
export const isNotAssignable = (checker: TypeChecker, nodes: Set<CallExpression>): Diagnostic[] => {
14+
const diagnostics: Diagnostic[] = [];
15+
16+
if (!nodes) {
17+
return diagnostics;
18+
}
19+
20+
for (const node of nodes) {
21+
if (!node.typeArguments) {
22+
// Skip if the node does not have generics
23+
continue;
24+
}
25+
26+
// Retrieve the type to be expected. This is the type inside the generic.
27+
const expectedType = checker.getTypeFromTypeNode(node.typeArguments[0]);
28+
const argumentType = checker.getTypeAtLocation(node.arguments[0]);
29+
30+
if (checker.isTypeAssignableTo(argumentType, expectedType)) {
31+
/**
32+
* The argument type is assignable to the expected type, we don't want this so add a diagnostic.
33+
*/
34+
diagnostics.push(makeDiagnostic(node, `Argument of type \`${checker.typeToString(argumentType)}\` is assignable to parameter of type \`${checker.typeToString(expectedType)}\`.`));
35+
}
36+
}
37+
38+
return diagnostics;
39+
};

source/lib/assertions/handlers/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export {Handler} from './handler';
22

33
// Handlers
44
export {strictAssertion} from './strict-assertion';
5+
export {isNotAssignable} from './assignability';

source/lib/assertions/index.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ import {CallExpression} from '../../../libraries/typescript/lib/typescript';
22
import {TypeChecker} from '../entities/typescript';
33
import {Diagnostic} from '../interfaces';
44
import {Handler, strictAssertion} from './handlers';
5+
import {isNotAssignable} from './handlers/assignability';
56

67
export enum Assertion {
78
EXPECT_TYPE = 'expectType',
8-
EXPECT_ERROR = 'expectError'
9+
EXPECT_ERROR = 'expectError',
10+
EXPECT_ASSIGNABLE = 'expectAssignable',
11+
EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable'
912
}
1013

1114
// List of diagnostic handlers attached to the assertion
1215
const assertionHandlers = new Map<string, Handler | Handler[]>([
13-
[Assertion.EXPECT_TYPE, strictAssertion]
16+
[Assertion.EXPECT_TYPE, strictAssertion],
17+
[Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable]
1418
]);
1519

1620
/**

source/test/assignability.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as path from 'path';
2+
import test from 'ava';
3+
import {verify} from './fixtures/utils';
4+
import tsd from '..';
5+
6+
test('assignable', async t => {
7+
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/assignability/assignable')});
8+
9+
verify(t, diagnostics, [
10+
[8, 26, 'error', 'Argument of type \'string\' is not assignable to parameter of type \'boolean\'.']
11+
]);
12+
});
13+
14+
test('not assignable', async t => {
15+
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/assignability/not-assignable')});
16+
17+
verify(t, diagnostics, [
18+
[4, 0, 'error', 'Argument of type `string` is assignable to parameter of type `string | number`.'],
19+
[5, 0, 'error', 'Argument of type `string` is assignable to parameter of type `any`.'],
20+
]);
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare const concat: {
2+
(foo: string, bar: string): string;
3+
(foo: number, bar: number): number;
4+
};
5+
6+
export default concat;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports.default = (foo, bar) => {
2+
return foo + bar;
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {expectAssignable} from '../../../..';
2+
import concat from '.';
3+
4+
expectAssignable<string | number>(concat('foo', 'bar'));
5+
expectAssignable<string | number>(concat(1, 2));
6+
expectAssignable<any>(concat(1, 2));
7+
8+
expectAssignable<boolean>(concat('unicorn', 'rainbow'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "foo"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare const concat: {
2+
(foo: string, bar: string): string;
3+
(foo: number, bar: number): number;
4+
};
5+
6+
export default concat;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports.default = (foo, bar) => {
2+
return foo + bar;
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {expectNotAssignable} from '../../../..';
2+
import concat from '.';
3+
4+
expectNotAssignable<string | number>(concat('foo', 'bar'));
5+
expectNotAssignable<any>(concat('foo', 'bar'));
6+
7+
expectNotAssignable<boolean>(concat('unicorn', 'rainbow'));
8+
expectNotAssignable<symbol>(concat('unicorn', 'rainbow'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "foo"
3+
}

source/test/fixtures/utils.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {ExecutionContext} from 'ava';
2+
import {Diagnostic} from '../../lib/interfaces';
3+
4+
type Expectation = [number, number, 'error' | 'warning', string, (string | RegExp)?];
5+
6+
/**
7+
* Verify a list of diagnostics.
8+
*
9+
* @param t - The AVA execution context.
10+
* @param diagnostics - List of diagnostics to verify.
11+
* @param expectations - Expected diagnostics.
12+
*/
13+
export const verify = (t: ExecutionContext, diagnostics: Diagnostic[], expectations: Expectation[]) => {
14+
t.true(diagnostics.length === expectations.length);
15+
16+
for (const [index, diagnostic] of diagnostics.entries()) {
17+
t.is(diagnostic.line, expectations[index][0]);
18+
t.is(diagnostic.column, expectations[index][1]);
19+
t.is(diagnostic.severity, expectations[index][2]);
20+
t.is(diagnostic.message, expectations[index][3]);
21+
22+
const filename = expectations[index][4];
23+
24+
if (typeof filename === 'string') {
25+
t.is(diagnostic.fileName, filename);
26+
} else if (typeof filename === 'object') {
27+
t.regex(diagnostic.fileName, filename);
28+
}
29+
}
30+
};

source/test/test.ts

+2-30
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,7 @@
11
import * as path from 'path';
2-
import test, {ExecutionContext} from 'ava';
2+
import test from 'ava';
3+
import {verify} from './fixtures/utils';
34
import tsd from '..';
4-
import {Diagnostic} from '../lib/interfaces';
5-
6-
type Expectation = [number, number, 'error' | 'warning', string, (string | RegExp)?];
7-
8-
/**
9-
* Verify a list of diagnostics.
10-
*
11-
* @param t - The AVA execution context.
12-
* @param diagnostics - List of diagnostics to verify.
13-
* @param expectations - Expected diagnostics.
14-
*/
15-
const verify = (t: ExecutionContext, diagnostics: Diagnostic[], expectations: Expectation[]) => {
16-
t.true(diagnostics.length === expectations.length);
17-
18-
for (const [index, diagnostic] of diagnostics.entries()) {
19-
t.is(diagnostic.line, expectations[index][0]);
20-
t.is(diagnostic.column, expectations[index][1]);
21-
t.is(diagnostic.severity, expectations[index][2]);
22-
t.is(diagnostic.message, expectations[index][3]);
23-
24-
const filename = expectations[index][4];
25-
26-
if (typeof filename === 'string') {
27-
t.is(diagnostic.fileName, filename);
28-
} else if (typeof filename === 'object') {
29-
t.regex(diagnostic.fileName, filename);
30-
}
31-
}
32-
};
335

346
test('throw if no type definition was found', async t => {
357
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.');

0 commit comments

Comments
 (0)