diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index c4507267c6f..a0b38fead9c 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -119,6 +119,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | | | :thought_balloon: |
| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :heavy_check_mark: | :wrench: | |
+| [`@typescript-eslint/no-invalid-void-type`](./docs/rules/no-invalid-void-type.md) | Disallows usage of `void` type outside of generic or return types | | | |
| [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :heavy_check_mark: | | |
| [`@typescript-eslint/no-misused-promises`](./docs/rules/no-misused-promises.md) | Avoid using promises in places not designed to handle them | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow the use of custom TypeScript modules and namespaces | :heavy_check_mark: | | |
diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md
index c6348ab2658..ea2837727b3 100644
--- a/packages/eslint-plugin/ROADMAP.md
+++ b/packages/eslint-plugin/ROADMAP.md
@@ -18,7 +18,7 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th
| [`adjacent-overload-signatures`] | ✅ | [`@typescript-eslint/adjacent-overload-signatures`] |
| [`ban-ts-ignore`] | ✅ | [`@typescript-eslint/ban-ts-ignore`] |
| [`ban-types`] | 🌓 | [`@typescript-eslint/ban-types`][1] |
-| [`invalid-void`] | 🛑 | N/A |
+| [`invalid-void`] | ✅ | [`@typescript-eslint/no-invalid-void-type`] |
| [`member-access`] | ✅ | [`@typescript-eslint/explicit-member-accessibility`] |
| [`member-ordering`] | ✅ | [`@typescript-eslint/member-ordering`] |
| [`no-any`] | ✅ | [`@typescript-eslint/no-explicit-any`] |
@@ -623,6 +623,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
[`@typescript-eslint/restrict-plus-operands`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/restrict-plus-operands.md
[`@typescript-eslint/strict-boolean-expressions`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md
[`@typescript-eslint/indent`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/indent.md
+[`@typescript-eslint/no-invalid-void-type`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-invalid-void-type.md
[`@typescript-eslint/no-require-imports`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-require-imports.md
[`@typescript-eslint/array-type`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/array-type.md
[`@typescript-eslint/class-name-casing`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/class-name-casing.md
diff --git a/packages/eslint-plugin/docs/rules/no-invalid-void-type.md b/packages/eslint-plugin/docs/rules/no-invalid-void-type.md
new file mode 100644
index 00000000000..9cc9808852f
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/no-invalid-void-type.md
@@ -0,0 +1,101 @@
+# Disallows usage of `void` type outside of generic or return types (`no-invalid-void-type`)
+
+Disallows usage of `void` type outside of return types or generic type arguments.
+If `void` is used as return type, it shouldn’t be a part of intersection/union type.
+
+## Rationale
+
+The `void` type means “nothing” or that a function does not return any value,
+in contrast with implicit `undefined` type which means that a function returns a value `undefined`.
+So “nothing” cannot be mixed with any other types. If you need this - use the `undefined` type instead.
+
+## Rule Details
+
+This rule aims to ensure that the `void` type is only used in valid places.
+
+The following patterns are considered warnings:
+
+```ts
+type PossibleValues = string | number | void;
+type MorePossibleValues = string | ((number & any) | (string | void));
+
+function logSomething(thing: void) {}
+function printArg(arg: T) {}
+
+logAndReturn(undefined);
+
+interface Interface {
+ lambda: () => void;
+ prop: void;
+}
+
+class MyClass {
+ private readonly propName: void;
+}
+```
+
+The following patterns are not considered warnings:
+
+```ts
+type NoOp = () => void;
+
+function noop(): void {}
+
+let trulyUndefined = void 0;
+
+async function promiseMeSomething(): Promise {}
+```
+
+### Options
+
+```ts
+interface Options {
+ allowInGenericTypeArguments?: boolean | string[];
+}
+
+const defaultOptions: Options = {
+ allowInGenericTypeArguments: true,
+};
+```
+
+#### `allowInGenericTypeArguments`
+
+This option lets you control if `void` can be used as a valid value for generic type parameters.
+
+Alternatively, you can provide an array of strings which whitelist which types may accept `void` as a generic type parameter.
+
+This option is `true` by default.
+
+The following patterns are considered warnings with `{ allowInGenericTypeArguments: false }`:
+
+```ts
+logAndReturn(undefined);
+
+let voidPromise: Promise = new Promise(() => {});
+let voidMap: Map = new Map();
+```
+
+The following patterns are considered warnings with `{ allowInGenericTypeArguments: ['Ex.Mx.Tx'] }`:
+
+```ts
+logAndReturn(undefined);
+
+type NotAllowedVoid1 = Mx.Tx;
+type NotAllowedVoid2 = Tx;
+type NotAllowedVoid3 = Promise;
+```
+
+The following patterns are not considered warnings with `{ allowInGenericTypeArguments: ['Ex.Mx.Tx'] }`:
+
+```ts
+type AllowedVoid = Ex.MX.Tx;
+```
+
+## When Not To Use It
+
+If you don't care about if `void` is used with other types,
+or in invalid places, then you don't need this rule.
+
+## Compatibility
+
+- TSLint: [invalid-void](https://palantir.github.io/tslint/rules/invalid-void/)
diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json
index 99e6060d670..7a6a217140a 100644
--- a/packages/eslint-plugin/src/configs/all.json
+++ b/packages/eslint-plugin/src/configs/all.json
@@ -50,6 +50,7 @@
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-inferrable-types": "error",
+ "@typescript-eslint/no-invalid-void-type": "error",
"no-magic-numbers": "off",
"@typescript-eslint/no-magic-numbers": "error",
"@typescript-eslint/no-misused-new": "error",
diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts
index cbc3b8c68f9..b6652f79eb4 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -40,6 +40,7 @@ import noFloatingPromises from './no-floating-promises';
import noForInArray from './no-for-in-array';
import noImpliedEval from './no-implied-eval';
import noInferrableTypes from './no-inferrable-types';
+import noInvalidVoidType from './no-invalid-void-type';
import noMagicNumbers from './no-magic-numbers';
import noMisusedNew from './no-misused-new';
import noMisusedPromises from './no-misused-promises';
@@ -142,6 +143,7 @@ export default {
'no-for-in-array': noForInArray,
'no-implied-eval': noImpliedEval,
'no-inferrable-types': noInferrableTypes,
+ 'no-invalid-void-type': noInvalidVoidType,
'no-magic-numbers': noMagicNumbers,
'no-misused-new': noMisusedNew,
'no-misused-promises': noMisusedPromises,
diff --git a/packages/eslint-plugin/src/rules/no-invalid-void-type.ts b/packages/eslint-plugin/src/rules/no-invalid-void-type.ts
new file mode 100644
index 00000000000..7d5dea30033
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-invalid-void-type.ts
@@ -0,0 +1,116 @@
+import {
+ AST_NODE_TYPES,
+ TSESTree,
+} from '@typescript-eslint/experimental-utils';
+import * as util from '../util';
+
+interface Options {
+ allowInGenericTypeArguments: boolean | string[];
+}
+
+type MessageIds =
+ | 'invalidVoidForGeneric'
+ | 'invalidVoidNotReturnOrGeneric'
+ | 'invalidVoidNotReturn';
+
+export default util.createRule<[Options], MessageIds>({
+ name: 'no-invalid-void-type',
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'Disallows usage of `void` type outside of generic or return types',
+ category: 'Best Practices',
+ recommended: false,
+ },
+ messages: {
+ invalidVoidForGeneric:
+ '{{ generic }} may not have void as a type variable',
+ invalidVoidNotReturnOrGeneric:
+ 'void is only valid as a return type or generic type variable',
+ invalidVoidNotReturn: 'void is only valid as a return type',
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ allowInGenericTypeArguments: {
+ oneOf: [
+ { type: 'boolean' },
+ {
+ type: 'array',
+ items: { type: 'string' },
+ minLength: 1,
+ },
+ ],
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ },
+ defaultOptions: [{ allowInGenericTypeArguments: true }],
+ create(context, [{ allowInGenericTypeArguments }]) {
+ const validParents: AST_NODE_TYPES[] = [
+ AST_NODE_TYPES.TSTypeAnnotation, //
+ ];
+ const invalidGrandParents: AST_NODE_TYPES[] = [
+ AST_NODE_TYPES.TSPropertySignature,
+ AST_NODE_TYPES.CallExpression,
+ AST_NODE_TYPES.ClassProperty,
+ AST_NODE_TYPES.Identifier,
+ ];
+
+ if (allowInGenericTypeArguments === true) {
+ validParents.push(AST_NODE_TYPES.TSTypeParameterInstantiation);
+ }
+
+ return {
+ TSVoidKeyword(node: TSESTree.TSVoidKeyword): void {
+ /* istanbul ignore next */
+ if (!node.parent?.parent) {
+ return;
+ }
+
+ if (
+ validParents.includes(node.parent.type) &&
+ !invalidGrandParents.includes(node.parent.parent.type)
+ ) {
+ return;
+ }
+
+ if (
+ node.parent.type === AST_NODE_TYPES.TSTypeParameterInstantiation &&
+ node.parent.parent.type === AST_NODE_TYPES.TSTypeReference &&
+ Array.isArray(allowInGenericTypeArguments)
+ ) {
+ const sourceCode = context.getSourceCode();
+ const fullyQualifiedName = sourceCode
+ .getText(node.parent.parent.typeName)
+ .replace(/ /gu, '');
+
+ if (
+ !allowInGenericTypeArguments
+ .map(s => s.replace(/ /gu, ''))
+ .includes(fullyQualifiedName)
+ ) {
+ context.report({
+ messageId: 'invalidVoidForGeneric',
+ data: { generic: fullyQualifiedName },
+ node,
+ });
+ }
+
+ return;
+ }
+
+ context.report({
+ messageId: allowInGenericTypeArguments
+ ? 'invalidVoidNotReturnOrGeneric'
+ : 'invalidVoidNotReturn',
+ node,
+ });
+ },
+ };
+ },
+});
diff --git a/packages/eslint-plugin/tests/rules/no-invalid-void-type.test.ts b/packages/eslint-plugin/tests/rules/no-invalid-void-type.test.ts
new file mode 100644
index 00000000000..057da9b7e84
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/no-invalid-void-type.test.ts
@@ -0,0 +1,476 @@
+import rule from '../../src/rules/no-invalid-void-type';
+import { RuleTester } from '../RuleTester';
+
+const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+});
+
+ruleTester.run('allowInGenericTypeArguments: false', rule, {
+ valid: [
+ {
+ code: 'type Generic = [T];',
+ options: [{ allowInGenericTypeArguments: false }],
+ },
+ ],
+ invalid: [
+ {
+ code: 'type GenericVoid = Generic;',
+ options: [{ allowInGenericTypeArguments: false }],
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturn',
+ line: 1,
+ column: 28,
+ },
+ ],
+ },
+ {
+ code: 'function takeVoid(thing: void) {}',
+ options: [{ allowInGenericTypeArguments: false }],
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturn',
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ {
+ code: 'let voidPromise: Promise = new Promise(() => {});',
+ options: [{ allowInGenericTypeArguments: false }],
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturn',
+ line: 1,
+ column: 26,
+ },
+ {
+ messageId: 'invalidVoidNotReturn',
+ line: 1,
+ column: 46,
+ },
+ ],
+ },
+ {
+ code: 'let voidMap: Map = new Map();',
+ options: [{ allowInGenericTypeArguments: false }],
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturn',
+ line: 1,
+ column: 26,
+ },
+ {
+ messageId: 'invalidVoidNotReturn',
+ line: 1,
+ column: 50,
+ },
+ ],
+ },
+ ],
+});
+
+ruleTester.run('allowInGenericTypeArguments: true', rule, {
+ valid: [
+ 'function func(): void {}',
+ 'type NormalType = () => void;',
+ 'let normalArrow = (): void => {};',
+ 'let ughThisThing = void 0;',
+ 'function takeThing(thing: undefined) {}',
+ 'takeThing(void 0);',
+ 'let voidPromise: Promise = new Promise(() => {});',
+ 'let voidMap: Map = new Map();',
+ `
+ function returnsVoidPromiseDirectly(): Promise {
+ return Promise.resolve();
+ }
+ `,
+ 'async function returnsVoidPromiseAsync(): Promise {}',
+ 'type UnionType = string | number;',
+ 'type GenericVoid = Generic;',
+ 'type Generic = [T];',
+ ],
+ invalid: [
+ {
+ code: 'function takeVoid(thing: void) {}',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ {
+ code: 'const arrowGeneric = (arg: T) => {};',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 33,
+ },
+ ],
+ },
+ {
+ code: 'const arrowGeneric1 = (arg: T) => {};',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 28,
+ },
+ ],
+ },
+ {
+ code: 'const arrowGeneric2 = (arg: T) => {};',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 34,
+ },
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 41,
+ },
+ ],
+ },
+ {
+ code: 'function functionGeneric(arg: T) {}',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 36,
+ },
+ ],
+ },
+ {
+ code: 'function functionGeneric1(arg: T) {}',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 31,
+ },
+ ],
+ },
+ {
+ code: 'function functionGeneric2(arg: T) {}',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 37,
+ },
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 44,
+ },
+ ],
+ },
+ {
+ code:
+ 'declare function functionDeclaration(arg: T): void;',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 48,
+ },
+ ],
+ },
+ {
+ code: 'declare function functionDeclaration1(arg: T): void;',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 43,
+ },
+ ],
+ },
+ {
+ code:
+ 'declare function functionDeclaration2(arg: T): void;',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 49,
+ },
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 56,
+ },
+ ],
+ },
+ {
+ code: 'functionGeneric(undefined);',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 17,
+ },
+ ],
+ },
+ {
+ code: 'declare function voidArray(args: void[]): void[];',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 34,
+ },
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 43,
+ },
+ ],
+ },
+ {
+ code: 'let value = undefined as void;',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ {
+ code: 'let value = undefined;',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 14,
+ },
+ ],
+ },
+ {
+ code: 'function takesThings(...things: void[]): void {}',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 33,
+ },
+ ],
+ },
+ {
+ code: 'type KeyofVoid = keyof void;',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 24,
+ },
+ ],
+ },
+ {
+ code: `
+ interface Interface {
+ lambda: () => void;
+ voidProp: void;
+ }
+ `,
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 4,
+ column: 21,
+ },
+ ],
+ },
+ {
+ code: `
+ class ClassName {
+ private readonly propName: void;
+ }
+ `,
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 3,
+ column: 38,
+ },
+ ],
+ },
+ {
+ code: 'let letVoid: void;',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 14,
+ },
+ ],
+ },
+ {
+ code: `
+ type VoidType = void;
+
+ class OtherClassName {
+ private propName: VoidType;
+ }
+ `,
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 2,
+ column: 25,
+ },
+ ],
+ },
+ {
+ code: 'type UnionType2 = string | number | void;',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 37,
+ },
+ ],
+ },
+ {
+ code: 'type UnionType3 = string | ((number & any) | (string | void));',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 56,
+ },
+ ],
+ },
+ {
+ code: 'type IntersectionType = string & number & void;',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 43,
+ },
+ ],
+ },
+ {
+ code: `
+ type MappedType = {
+ [K in keyof T]: void;
+ };
+ `,
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 3,
+ column: 27,
+ },
+ ],
+ },
+ {
+ code: `
+ type ConditionalType = {
+ [K in keyof T]: T[K] extends string ? void : string;
+ };
+ `,
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 3,
+ column: 49,
+ },
+ ],
+ },
+ {
+ code: 'type ManyVoid = readonly void[];',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ {
+ code: 'function foo(arr: readonly void[]) {}',
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 28,
+ },
+ ],
+ },
+ ],
+});
+
+ruleTester.run('allowInGenericTypeArguments: whitelist', rule, {
+ valid: [
+ 'type Allowed = [T];',
+ 'type Banned = [T];',
+ {
+ code: 'type AllowedVoid = Allowed;',
+ options: [{ allowInGenericTypeArguments: ['Allowed'] }],
+ },
+ {
+ code: 'type AllowedVoid = Ex.Mx.Tx;',
+ options: [{ allowInGenericTypeArguments: ['Ex.Mx.Tx'] }],
+ },
+ {
+ // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
+ code: 'type AllowedVoid = Ex . Mx . Tx;',
+ options: [{ allowInGenericTypeArguments: ['Ex.Mx.Tx'] }],
+ },
+ {
+ // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
+ code: 'type AllowedVoid = Ex . Mx . Tx;',
+ options: [{ allowInGenericTypeArguments: ['Ex.Mx . Tx'] }],
+ },
+ {
+ code: 'type AllowedVoid = Ex.Mx.Tx;',
+ options: [{ allowInGenericTypeArguments: ['Ex . Mx . Tx'] }],
+ },
+ ],
+ invalid: [
+ {
+ code: 'type BannedVoid = Banned;',
+ options: [{ allowInGenericTypeArguments: ['Allowed'] }],
+ errors: [
+ {
+ messageId: 'invalidVoidForGeneric',
+ data: { generic: 'Banned' },
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ {
+ code: 'type BannedVoid = Ex.Mx.Tx;',
+ options: [{ allowInGenericTypeArguments: ['Tx'] }],
+ errors: [
+ {
+ messageId: 'invalidVoidForGeneric',
+ data: { generic: 'Ex.Mx.Tx' },
+ line: 1,
+ column: 28,
+ },
+ ],
+ },
+ {
+ code: 'function takeVoid(thing: void) {}',
+ options: [{ allowInGenericTypeArguments: ['Allowed'] }],
+ errors: [
+ {
+ messageId: 'invalidVoidNotReturnOrGeneric',
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ ],
+});