Skip to content

Commit 861db08

Browse files
Add expectNotType assertion - fixes #56
1 parent 67ae75b commit 861db08

File tree

14 files changed

+165
-3
lines changed

14 files changed

+165
-3
lines changed

readme.md

+4
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ These options will be overridden if a `tsconfig.json` file is found in your proj
154154

155155
Check that the type of `value` is identical to type `T`.
156156

157+
### expectNotType<T>(value)
158+
159+
Check that the type of `value` is not identical to type `T`.
160+
157161
### expectAssignable<T>(value)
158162

159163
Check that the type of `value` is assignable to type `T`.

source/lib/assertions/assert.ts

+11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ 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 not identical to type `T`.
13+
*
14+
* @param value - Value that should be identical to type `T`.
15+
*/
16+
// @ts-ignore
17+
export const expectNotType = <T>(value: any) => { // tslint:disable-line:no-unused
18+
// TODO Use a `not T` type when possible https://github.com/microsoft/TypeScript/pull/29317
19+
// Do nothing, the TypeScript compiler handles this for us
20+
};
21+
1122
/**
1223
* Check that the type of `value` is assignable to type `T`.
1324
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 identical 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 isIdentical = (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+
29+
// Retrieve the argument type. This is the type to be checked.
30+
const argumentType = checker.getTypeAtLocation(node.arguments[0]);
31+
32+
if (!checker.isTypeAssignableTo(argumentType, expectedType)) {
33+
// The argument type is not assignable to the expected type. TypeScript will catch this for us.
34+
continue;
35+
}
36+
37+
if (!checker.isTypeAssignableTo(expectedType, argumentType)) {
38+
/**
39+
* The expected type is not assignable to the argument type, but the argument type is
40+
* assignable to the expected type. This means our type is too wide.
41+
*/
42+
diagnostics.push(makeDiagnostic(node, `Parameter type \`${checker.typeToString(expectedType)}\` is declared too wide for argument type \`${checker.typeToString(argumentType)}\`.`));
43+
} else if (!checker.isTypeIdenticalTo(expectedType, argumentType)) {
44+
/**
45+
* The expected type and argument type are assignable in both directions. We still have to check
46+
* if the types are identical. See https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3.11.2.
47+
*/
48+
diagnostics.push(makeDiagnostic(node, `Parameter type \`${checker.typeToString(expectedType)}\` is not identical to argument type \`${checker.typeToString(argumentType)}\`.`));
49+
}
50+
}
51+
52+
return diagnostics;
53+
};
54+
55+
/**
56+
* Verifies that the argument of the assertion is not identical to the generic type of the assertion.
57+
*
58+
* @param checker - The TypeScript type checker.
59+
* @param nodes - The `expectType` AST nodes.
60+
* @return List of custom diagnostics.
61+
*/
62+
export const isNotIdentical = (checker: TypeChecker, nodes: Set<CallExpression>): Diagnostic[] => {
63+
const diagnostics: Diagnostic[] = [];
64+
65+
if (!nodes) {
66+
return diagnostics;
67+
}
68+
69+
for (const node of nodes) {
70+
if (!node.typeArguments) {
71+
// Skip if the node does not have generics
72+
continue;
73+
}
74+
75+
// Retrieve the type to be expected. This is the type inside the generic.
76+
const expectedType = checker.getTypeFromTypeNode(node.typeArguments[0]);
77+
const argumentType = checker.getTypeAtLocation(node.arguments[0]);
78+
79+
if (checker.isTypeIdenticalTo(expectedType, argumentType)) {
80+
diagnostics.push(makeDiagnostic(node, `Parameter type \`${checker.typeToString(expectedType)}\` is identical to argument type \`${checker.typeToString(argumentType)}\`.`));
81+
}
82+
}
83+
84+
return diagnostics;
85+
};
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export {Handler} from './handler';
22

33
// Handlers
4-
export {strictAssertion} from './strict-assertion';
4+
export {isIdentical, isNotIdentical} from './identicality';
55
export {isNotAssignable} from './assignability';
66
export {expectDeprecated, expectNotDeprecated} from './expect-deprecated';

source/lib/assertions/index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {CallExpression} from '../../../libraries/typescript/lib/typescript';
22
import {TypeChecker} from '../entities/typescript';
33
import {Diagnostic} from '../interfaces';
4-
import {Handler, strictAssertion, isNotAssignable, expectDeprecated, expectNotDeprecated} from './handlers';
4+
import {Handler, isIdentical, isNotIdentical, isNotAssignable, expectDeprecated, expectNotDeprecated} from './handlers';
55

66
export enum Assertion {
77
EXPECT_TYPE = 'expectType',
8+
EXPECT_NOT_TYPE = 'expectNotType',
89
EXPECT_ERROR = 'expectError',
910
EXPECT_ASSIGNABLE = 'expectAssignable',
1011
EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable',
@@ -14,7 +15,8 @@ export enum Assertion {
1415

1516
// List of diagnostic handlers attached to the assertion
1617
const assertionHandlers = new Map<string, Handler | Handler[]>([
17-
[Assertion.EXPECT_TYPE, strictAssertion],
18+
[Assertion.EXPECT_TYPE, isIdentical],
19+
[Assertion.EXPECT_NOT_TYPE, isNotIdentical],
1820
[Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable],
1921
[Assertion.EXPECT_DEPRECATED, expectDeprecated],
2022
[Assertion.EXPECT_NOT_DEPRECATED, expectNotDeprecated]
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 {expectType} from '../../../..';
2+
import concat from '.';
3+
4+
expectType<string>(concat('foo', 'bar'));
5+
expectType<number>(concat(1, 2));
6+
7+
expectType<any>(concat(1, 2));
8+
expectType<string | number>(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,7 @@
1+
import {expectNotType} from '../../../..';
2+
import concat from '.';
3+
4+
expectNotType<number>(concat('foo', 'bar'));
5+
expectNotType<string | number>(concat('foo', 'bar'));
6+
7+
expectNotType<string>(concat('unicorn', 'rainbow'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "foo"
3+
}

source/test/identicality.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('identical', async t => {
7+
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/identicality/identical')});
8+
9+
verify(t, diagnostics, [
10+
[7, 0, 'error', 'Parameter type `any` is declared too wide for argument type `number`.'],
11+
[8, 0, 'error', 'Parameter type `string | number` is declared too wide for argument type `string`.']
12+
]);
13+
});
14+
15+
test('not identical', async t => {
16+
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/identicality/not-identical')});
17+
18+
verify(t, diagnostics, [
19+
[7, 0, 'error', 'Parameter type `string` is identical to argument type `string`.']
20+
]);
21+
});

0 commit comments

Comments
 (0)