diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index 0428cff43fc..c4507267c6f 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -183,6 +183,7 @@ In these cases, we create what we call an extension rule; a rule within our plug
| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | |
| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | |
| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | |
+| [`@typescript-eslint/init-declarations`](./docs/rules/init-declarations.md) | require or disallow initialization in variable declarations | | | |
| [`@typescript-eslint/keyword-spacing`](./docs/rules/keyword-spacing.md) | Enforce consistent spacing before and after keywords | | :wrench: | |
| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/no-dupe-class-members`](./docs/rules/no-dupe-class-members.md) | Disallow duplicate class members | | | |
diff --git a/packages/eslint-plugin/docs/rules/init-declarations.md b/packages/eslint-plugin/docs/rules/init-declarations.md
new file mode 100644
index 00000000000..8888e2efef2
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/init-declarations.md
@@ -0,0 +1,22 @@
+# require or disallow initialization in variable declarations (`init-declarations`)
+
+## Rule Details
+
+This rule extends the base [`eslint/init-declarations`](https://eslint.org/docs/rules/init-declarations) rule.
+It adds support for TypeScript's `declare` variables.
+
+## How to use
+
+```cjson
+{
+ // note you must disable the base rule as it can report incorrect errors
+ "init-declarations": "off",
+ "@typescript-eslint/init-declarations": ["error"]
+}
+```
+
+## Options
+
+See [`eslint/init-declarations` options](https://eslint.org/docs/rules/init-declarations#options).
+
+Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/init-declarations.md)
diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json
index ec65f143c59..99e6060d670 100644
--- a/packages/eslint-plugin/src/configs/all.json
+++ b/packages/eslint-plugin/src/configs/all.json
@@ -22,6 +22,8 @@
"@typescript-eslint/func-call-spacing": "error",
"indent": "off",
"@typescript-eslint/indent": "error",
+ "init-declarations": "off",
+ "@typescript-eslint/init-declarations": "error",
"keyword-spacing": "off",
"@typescript-eslint/keyword-spacing": "error",
"@typescript-eslint/member-delimiter-style": "error",
diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts
index 63c402297e3..cbc3b8c68f9 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -87,6 +87,7 @@ import requireAwait from './require-await';
import restrictPlusOperands from './restrict-plus-operands';
import restrictTemplateExpressions from './restrict-template-expressions';
import returnAwait from './return-await';
+import initDeclarations from './init-declarations';
import semi from './semi';
import spaceBeforeFunctionParen from './space-before-function-paren';
import strictBooleanExpressions from './strict-boolean-expressions';
@@ -118,6 +119,7 @@ export default {
'func-call-spacing': funcCallSpacing,
'generic-type-naming': genericTypeNaming,
indent: indent,
+ 'init-declarations': initDeclarations,
'interface-name-prefix': interfaceNamePrefix,
'keyword-spacing': keywordSpacing,
'member-delimiter-style': memberDelimiterStyle,
diff --git a/packages/eslint-plugin/src/rules/init-declarations.ts b/packages/eslint-plugin/src/rules/init-declarations.ts
new file mode 100644
index 00000000000..b4368527e0c
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/init-declarations.ts
@@ -0,0 +1,53 @@
+import {
+ TSESTree,
+ AST_NODE_TYPES,
+} from '@typescript-eslint/experimental-utils';
+import baseRule from 'eslint/lib/rules/init-declarations';
+import {
+ InferOptionsTypeFromRule,
+ InferMessageIdsTypeFromRule,
+ createRule,
+} from '../util';
+
+export type Options = InferOptionsTypeFromRule;
+export type MessageIds = InferMessageIdsTypeFromRule;
+
+export default createRule({
+ name: 'init-declarations',
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description:
+ 'require or disallow initialization in variable declarations',
+ category: 'Variables',
+ recommended: false,
+ extendsBaseRule: true,
+ },
+ schema: baseRule.meta.schema,
+ messages: baseRule.meta.messages,
+ },
+ defaultOptions: ['always'],
+ create(context) {
+ const rules = baseRule.create(context);
+ const mode = context.options[0] || 'always';
+
+ return {
+ 'VariableDeclaration:exit'(node: TSESTree.VariableDeclaration): void {
+ if (mode === 'always') {
+ if (node.declare) {
+ return;
+ }
+ if (
+ node.parent?.type === AST_NODE_TYPES.TSModuleBlock &&
+ node.parent.parent?.type === AST_NODE_TYPES.TSModuleDeclaration &&
+ node.parent.parent?.declare
+ ) {
+ return;
+ }
+ }
+
+ rules['VariableDeclaration:exit'](node);
+ },
+ };
+ },
+});
diff --git a/packages/eslint-plugin/tests/rules/init-declarations.test.ts b/packages/eslint-plugin/tests/rules/init-declarations.test.ts
new file mode 100644
index 00000000000..6f4c8f75ebb
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/init-declarations.test.ts
@@ -0,0 +1,728 @@
+import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
+import rule from '../../src/rules/init-declarations';
+import { RuleTester } from '../RuleTester';
+
+const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+});
+
+ruleTester.run('init-declarations', rule, {
+ valid: [
+ // checking compatibility with base rule
+ 'var foo = null;',
+ 'foo = true;',
+ `
+var foo = 1,
+ bar = false,
+ baz = {};
+ `,
+ `
+function foo() {
+ var foo = 0;
+ var bar = [];
+}
+ `,
+ 'var fn = function() {};',
+ 'var foo = (bar = 2);',
+ 'for (var i = 0; i < 1; i++) {}',
+ `
+for (var foo in []) {
+}
+ `,
+ {
+ code: `
+for (var foo of []) {
+}
+ `,
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: 'let a = true;',
+ options: ['always'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: 'const a = {};',
+ options: ['always'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: `
+function foo() {
+ let a = 1,
+ b = false;
+ if (a) {
+ let c = 3,
+ d = null;
+ }
+}
+ `,
+ options: ['always'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: `
+function foo() {
+ const a = 1,
+ b = true;
+ if (a) {
+ const c = 3,
+ d = null;
+ }
+}
+ `,
+ options: ['always'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: `
+function foo() {
+ let a = 1;
+ const b = false;
+ var c = true;
+}
+ `,
+ options: ['always'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: 'var foo;',
+ options: ['never'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: 'var foo, bar, baz;',
+ options: ['never'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: `
+function foo() {
+ var foo;
+ var bar;
+}
+ `,
+ options: ['never'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: 'let a;',
+ options: ['never'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: 'const a = 1;',
+ options: ['never'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: `
+function foo() {
+ let a, b;
+ if (a) {
+ let c, d;
+ }
+}
+ `,
+ options: ['never'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: `
+function foo() {
+ const a = 1,
+ b = true;
+ if (a) {
+ const c = 3,
+ d = null;
+ }
+}
+ `,
+ options: ['never'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: `
+function foo() {
+ let a;
+ const b = false;
+ var c;
+}
+ `,
+ options: ['never'],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: 'for (var i = 0; i < 1; i++) {}',
+ options: ['never', { ignoreForLoopInit: true }],
+ },
+ {
+ code: `
+for (var foo in []) {
+}
+ `,
+ options: ['never', { ignoreForLoopInit: true }],
+ },
+ {
+ code: `
+for (var foo of []) {
+}
+ `,
+ options: ['never', { ignoreForLoopInit: true }],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ {
+ code: `
+function foo() {
+ var bar = 1;
+ let baz = 2;
+ const qux = 3;
+}
+ `,
+ options: ['always'],
+ },
+
+ // typescript-eslint
+ {
+ code: 'declare const foo: number;',
+ options: ['always'],
+ },
+ {
+ code: 'declare const foo: number;',
+ options: ['never'],
+ },
+ {
+ code: `
+declare namespace myLib {
+ let numberOfGreetings: number;
+}
+ `,
+ options: ['always'],
+ },
+ {
+ code: `
+declare namespace myLib {
+ let numberOfGreetings: number;
+}
+ `,
+ options: ['never'],
+ },
+ {
+ code: `
+interface GreetingSettings {
+ greeting: string;
+ duration?: number;
+ color?: string;
+}
+ `,
+ },
+ {
+ code: `
+interface GreetingSettings {
+ greeting: string;
+ duration?: number;
+ color?: string;
+}
+ `,
+ options: ['never'],
+ },
+ 'type GreetingLike = string | (() => string) | Greeter;',
+ {
+ code: 'type GreetingLike = string | (() => string) | Greeter;',
+ options: ['never'],
+ },
+ {
+ code: `
+function foo() {
+ var bar: string;
+}
+ `,
+ options: ['never'],
+ },
+ {
+ code: 'var bar: string;',
+ options: ['never'],
+ },
+ {
+ code: `
+var bar: string = function(): string {
+ return 'string';
+};
+ `,
+ options: ['always'],
+ },
+ {
+ code: `
+var bar: string = function(arg1: stirng): string {
+ return 'string';
+};
+ `,
+ options: ['always'],
+ },
+ {
+ code: "function foo(arg1: string = 'string'): void {}",
+ options: ['never'],
+ },
+ {
+ code: "const foo: string = 'hello';",
+ options: ['never'],
+ },
+ {
+ code: `
+const class1 = class NAME {
+ constructor() {
+ var name1: string = 'hello';
+ }
+};
+ `,
+ },
+ {
+ code: `
+const class1 = class NAME {
+ static pi: number = 3.14;
+};
+ `,
+ },
+ {
+ code: `
+const class1 = class NAME {
+ static pi: number = 3.14;
+};
+ `,
+ options: ['never'],
+ },
+ {
+ code: `
+interface IEmployee {
+ empCode: number;
+ empName: string;
+ getSalary: (number) => number; // arrow function
+ getManagerName(number): string;
+}
+ `,
+ },
+ {
+ code: `
+interface IEmployee {
+ empCode: number;
+ empName: string;
+ getSalary: (number) => number; // arrow function
+ getManagerName(number): string;
+}
+ `,
+ options: ['never'],
+ },
+ {
+ code: "declare const foo: number = 'asd';",
+ options: ['always'],
+ },
+
+ {
+ code: "const foo: number = 'asd';",
+ options: ['always'],
+ },
+ {
+ code: 'const foo: number;',
+ options: ['never'],
+ },
+ {
+ code: `
+namespace myLib {
+ let numberOfGreetings: number;
+}
+ `,
+ options: ['never'],
+ },
+ {
+ code: `
+namespace myLib {
+ let numberOfGreetings: number = 2;
+}
+ `,
+ options: ['always'],
+ },
+ ],
+ invalid: [
+ // checking compatibility with base rule
+ {
+ code: 'var foo;',
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'foo' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: 'for (var a in []) var foo;',
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'foo' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+var foo,
+ bar = false,
+ baz;
+ `,
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'foo' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ {
+ messageId: 'initialized',
+ data: { idName: 'baz' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+function foo() {
+ var foo = 0;
+ var bar;
+}
+ `,
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'bar' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+function foo() {
+ var foo;
+ var bar = foo;
+}
+ `,
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'foo' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: 'let a;',
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'a' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+function foo() {
+ let a = 1,
+ b;
+ if (a) {
+ let c = 3,
+ d = null;
+ }
+}
+ `,
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'b' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+function foo() {
+ let a;
+ const b = false;
+ var c;
+}
+ `,
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'a' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ {
+ messageId: 'initialized',
+ data: { idName: 'c' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: 'var foo = (bar = 2);',
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'foo' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: 'var foo = true;',
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'foo' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+var foo,
+ bar = 5,
+ baz = 3;
+ `,
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'bar' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'baz' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+function foo() {
+ var foo;
+ var bar = foo;
+}
+ `,
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'bar' },
+
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: 'let a = 1;',
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'a' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+function foo() {
+ let a = 'foo',
+ b;
+ if (a) {
+ let c, d;
+ }
+}
+ `,
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'a' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+function foo() {
+ let a;
+ const b = false;
+ var c = 1;
+}
+ `,
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'c' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: 'for (var i = 0; i < 1; i++) {}',
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'i' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+for (var foo in []) {
+}
+ `,
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'foo' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+for (var foo of []) {
+}
+ `,
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'foo' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+function foo() {
+ var bar;
+}
+ `,
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'bar' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+
+ // typescript-eslint
+ {
+ code: "let arr: string[] = ['arr', 'ar'];",
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'arr' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: 'let arr: string = function() {};',
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'arr' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+const class1 = class NAME {
+ constructor() {
+ var name1: string = 'hello';
+ }
+};
+ `,
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'name1' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: 'let arr: string;',
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'arr' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: "declare var foo: number = 'asd';",
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'foo' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+namespace myLib {
+ let numberOfGreetings: number;
+}
+ `,
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'initialized',
+ data: { idName: 'numberOfGreetings' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ {
+ code: `
+namespace myLib {
+ let numberOfGreetings: number = 2;
+}
+ `,
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'notInitialized',
+ data: { idName: 'numberOfGreetings' },
+ type: AST_NODE_TYPES.VariableDeclarator,
+ },
+ ],
+ },
+ ],
+});
diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts
index ac8700d42e5..eb2d1cd9c60 100644
--- a/packages/eslint-plugin/typings/eslint-rules.d.ts
+++ b/packages/eslint-plugin/typings/eslint-rules.d.ts
@@ -622,3 +622,21 @@ declare module 'eslint/lib/rules/no-extra-semi' {
>;
export = rule;
}
+
+declare module 'eslint/lib/rules/init-declarations' {
+ import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
+
+ const rule: TSESLint.RuleModule<
+ 'initialized' | 'notInitialized',
+ [
+ 'always' | 'never',
+ {
+ ignoreForLoopInit?: boolean;
+ }?,
+ ],
+ {
+ 'VariableDeclaration:exit'(node: TSESTree.VariableDeclaration): void;
+ }
+ >;
+ export = rule;
+}