diff --git a/readme.md b/readme.md index 17c87a20..0fff1e9f 100644 --- a/readme.md +++ b/readme.md @@ -193,6 +193,12 @@ Prints the type of `expression` as a warning. Useful if you don't know the exact type of the expression passed to `printType()` or the type is too complex to write out by hand. +### expectNever(expression: never) + +Asserts that the type and return type of `expression` is `never`. + +Useful for checking that all branches are covered. + ### expectDocCommentIncludes<T>(expression: any) Asserts that the documentation comment of `expression` includes string literal type `T`. diff --git a/source/lib/assertions/assert.ts b/source/lib/assertions/assert.ts index d1c69208..0116ee1f 100644 --- a/source/lib/assertions/assert.ts +++ b/source/lib/assertions/assert.ts @@ -72,6 +72,17 @@ export const expectNotDeprecated = (expression: any) => { // Do nothing, the TypeScript compiler handles this for us }; +/** + * Asserts that the type and return type of `expression` is `never`. + * + * Useful for checking that all branches are covered. + * + * @param expression - Expression that should be `never`. + */ +export const expectNever = (expression: never): never => { + return expression; +}; + /** * Prints the type of `expression` as a warning. * diff --git a/source/lib/assertions/handlers/identicality.ts b/source/lib/assertions/handlers/identicality.ts index 33c4e928..f375c134 100644 --- a/source/lib/assertions/handlers/identicality.ts +++ b/source/lib/assertions/handlers/identicality.ts @@ -1,4 +1,4 @@ -import {CallExpression, TypeChecker} from '@tsd/typescript'; +import {CallExpression, TypeChecker, TypeFlags} from '@tsd/typescript'; import {Diagnostic} from '../../interfaces'; import {makeDiagnostic} from '../../utils'; @@ -82,3 +82,28 @@ export const isNotIdentical = (checker: TypeChecker, nodes: Set) return diagnostics; }; + +/** + * Verifies that the argument of the assertion is `never` + * + * @param checker - The TypeScript type checker. + * @param nodes - The `expectNever` AST nodes. + * @return List of custom diagnostics. + */ +export const isNever = (checker: TypeChecker, nodes: Set): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + if (!nodes) { + return diagnostics; + } + + for (const node of nodes) { + const argumentType = checker.getTypeAtLocation(node.arguments[0]); + + if (argumentType.flags !== TypeFlags.Never) { + diagnostics.push(makeDiagnostic(node, `Argument of type \`${checker.typeToString(argumentType)}\` is not \`never\`.`)); + } + } + + return diagnostics; +}; diff --git a/source/lib/assertions/handlers/index.ts b/source/lib/assertions/handlers/index.ts index 058eb784..8386e765 100644 --- a/source/lib/assertions/handlers/index.ts +++ b/source/lib/assertions/handlers/index.ts @@ -1,7 +1,7 @@ export {Handler} from './handler'; // Handlers -export {isIdentical, isNotIdentical} from './identicality'; +export {isIdentical, isNotIdentical, isNever} from './identicality'; export {isNotAssignable} from './assignability'; export {expectDeprecated, expectNotDeprecated} from './expect-deprecated'; export {printTypeWarning, expectDocCommentIncludes} from './informational'; diff --git a/source/lib/assertions/index.ts b/source/lib/assertions/index.ts index b51d8a54..6cf6f8e9 100644 --- a/source/lib/assertions/index.ts +++ b/source/lib/assertions/index.ts @@ -7,6 +7,7 @@ import { isNotAssignable, expectDeprecated, expectNotDeprecated, + isNever, printTypeWarning, expectDocCommentIncludes, } from './handlers'; @@ -19,6 +20,7 @@ export enum Assertion { EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable', EXPECT_DEPRECATED = 'expectDeprecated', EXPECT_NOT_DEPRECATED = 'expectNotDeprecated', + EXPECT_NEVER = 'expectNever', PRINT_TYPE = 'printType', EXPECT_DOC_COMMENT_INCLUDES = 'expectDocCommentIncludes', } @@ -30,6 +32,7 @@ const assertionHandlers = new Map([ [Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable], [Assertion.EXPECT_DEPRECATED, expectDeprecated], [Assertion.EXPECT_NOT_DEPRECATED, expectNotDeprecated], + [Assertion.EXPECT_NEVER, isNever], [Assertion.PRINT_TYPE, printTypeWarning], [Assertion.EXPECT_DOC_COMMENT_INCLUDES, expectDocCommentIncludes], ]); diff --git a/source/test/fixtures/identicality/identical/index.d.ts b/source/test/fixtures/identicality/identical/index.d.ts index 266914ab..e5e47847 100644 --- a/source/test/fixtures/identicality/identical/index.d.ts +++ b/source/test/fixtures/identicality/identical/index.d.ts @@ -4,3 +4,5 @@ declare const concat: { }; export default concat; + +export const returnsNever: () => never; diff --git a/source/test/fixtures/identicality/identical/index.js b/source/test/fixtures/identicality/identical/index.js index f17717f5..0162a427 100644 --- a/source/test/fixtures/identicality/identical/index.js +++ b/source/test/fixtures/identicality/identical/index.js @@ -1,3 +1,5 @@ module.exports.default = (foo, bar) => { return foo + bar; }; + +module.exports.returnsNever = () => {}; diff --git a/source/test/fixtures/identicality/identical/index.test-d.ts b/source/test/fixtures/identicality/identical/index.test-d.ts index 1af8c764..940734b1 100644 --- a/source/test/fixtures/identicality/identical/index.test-d.ts +++ b/source/test/fixtures/identicality/identical/index.test-d.ts @@ -1,5 +1,5 @@ -import {expectType} from '../../../..'; -import concat from '.'; +import {expectType, expectNever} from '../../../..'; +import concat, {returnsNever} from '.'; expectType(concat('foo', 'bar')); expectType(concat(1, 2)); @@ -11,3 +11,6 @@ expectType(concat(1, 2) as any); expectType('' as never); expectType('' as never); + +expectNever(returnsNever()); +expectNever(5 as number); diff --git a/source/test/identicality.ts b/source/test/identicality.ts index 6f7ec197..4d9abec4 100644 --- a/source/test/identicality.ts +++ b/source/test/identicality.ts @@ -12,6 +12,8 @@ test('identical', async t => { [10, 0, 'error', 'Parameter type `false` is not identical to argument type `any`.'], [12, 0, 'error', 'Parameter type `string` is declared too wide for argument type `never`.'], [13, 0, 'error', 'Parameter type `any` is declared too wide for argument type `never`.'], + [16, 0, 'error', 'Argument of type `number` is not `never`.'], + [16, 12, 'error', 'Argument of type \'number\' is not assignable to parameter of type \'never\'.'], ]); });