Skip to main content

Variables & names

This page discusses rules around variable declaration, use of properties, and naming.

Variable declarations

block-scoped-var

  • Severity: error

Because vars are forbidden altogether, this rule is mostly moot. In the rare case where you need to use var (such as to declare globals), such vars should not be deceptively inside a block.

init-declarations

We require variables to be initialized. Otherwise, it's possible to circumvent TypeScript:

ts
let a: number;
function useA() {
console.log(a); // -> undefined
}
useA();
a = 1;
ts
let a: number;
function useA() {
console.log(a); // -> undefined
}
useA();
a = 1;

You should almost always initialize variables upfront (and use const where possible). Use ternaries instead of if...else. If you need to lazy initialize a variable, initialize it to undefined, so that you remember to explicitly check for undefined before using it.

no-const-assign

  • Severity: error
  • Related:
    • ts(2588): Cannot assign to 'a' because it is a constant.

Re-assigning const variables causes a runtime error.

ts
const a = 1;
a = 2; // -> TypeError: Assignment to constant variable.
Cannot assign to 'a' because it is a constant.2588Cannot assign to 'a' because it is a constant.
ts
const a = 1;
a = 2; // -> TypeError: Assignment to constant variable.
Cannot assign to 'a' because it is a constant.2588Cannot assign to 'a' because it is a constant.

no-implicit-globals

  • Severity: error
  • Configuration:
    • Disallow global lexical declarations too (lexicalBindings: true)

We have forbidden using var. let and const at the top level also behave weirdly due to TDZ. You should probably be modularizing your code anyway.

no-redeclare

Do not redeclare var/function. This is probably a mistake. Note that let/const cannot be redeclared and doing so is a syntax error in the first place.

no-shadow

  • Severity: warning
  • Configuration:
    • Ignore shadowing of globals (builtinGlobals: true)
    • Check shadowing of all variables declared in the outer scope (hoist: "all")
    • Allow shadowing of uninitialized variables (ignoreOnInitialization: true)

We avoid shadowing because doing so is a refactoring hazard.

ts
function foo(x) {
doSomething((x) => {
console.log(x); // What is this x meant to be?
// If I change the parameter name, should I change this too?
});
}
ts
function foo(x) {
doSomething((x) => {
console.log(x); // What is this x meant to be?
// If I change the parameter name, should I change this too?
});
}

The issue isn't better because the variable is only declared afterwards.

ts
function foo() {
doSomething((x) => {
console.log(x); // What is this x meant to be?
// If I change the parameter name, should I change this too?
});
// If I move this declaration before doSomething(), there
// shouldn't be a difference
const x = 1;
}
ts
function foo() {
doSomething((x) => {
console.log(x); // What is this x meant to be?
// If I change the parameter name, should I change this too?
});
// If I move this declaration before doSomething(), there
// shouldn't be a difference
const x = 1;
}

However, shadowing is allowed when the variable is initialized later. The following pattern is encouraged:

ts
const x = (() => {
let x = 0;
// ...
return x;
})();
ts
const x = (() => {
let x = 0;
// ...
return x;
})();

We allow shadowing globals—this is for a pragmatic concern. There are some extremely generically named globals like name and Plugin which we don't want to prevented from being used as local variables. However, you should probably avoid using names like fetch.

no-var

We disallow var statements. var is fully predated by let/const and its hoisting behavior makes code harder to debug. There's not a single reason to use var today. If you need to share one variable between two blocks, declare it in the upper scope. If you need to declare a global variable (which you probably shouldn't anyway), directly modify globalThis (which also works in modules).

ts
declare var globalVar: number;
globalThis.globalVar = 1;
ts
declare var globalVar: number;
globalThis.globalVar = 1;

no-undef-init

  • Severity: off

We require variables to be initialized (through init-declarations). In case there's no reasonable default value, you should use explicit undefined.

no-unused-vars

  • Severity: error
  • Configuration:
    • Check unused trailing function parameters (args: "after-used")
    • Check unused caught errors (caughtErrors: "all")
    • Ignore unused variables with rest element (ignoreRestSiblings: true)
    • Check unused variables in the top-level scope (vars: "all")
  • Related:

Unused variables are a sign of refactoring artifact and should be removed as early as possible. However, there are the following exceptions:

tsx
// Satisfying a type signature
const plugin: Plugin = (ast, options) => {
// Only use options, but ast has to be declared
};
// Removing properties from objects
function Component(props: Props) {
const { someProp, ...rest } = props;
return <div {...rest} />;
}
tsx
// Satisfying a type signature
const plugin: Plugin = (ast, options) => {
// Only use options, but ast has to be declared
};
// Removing properties from objects
function Component(props: Props) {
const { someProp, ...rest } = props;
return <div {...rest} />;
}

If you have an unused error variable, omit the catch binding.

ts
try {
// ...
} catch {
console.error("Failed");
}
ts
try {
// ...
} catch {
console.error("Failed");
}

no-use-before-define

  • Severity: warning
  • Configuration:
    • Allow export declarations before declarations (allowNamedExports: false)
    • Check class declarations (classes: true)
    • Allow function declarations to be hoisted (functions: true)
    • Check variable declarations (variables: true)

You should generally avoid using variables before they are declared, as doing so leads to an error. For functions, you are free to let them get hoisted. In fact, we recommend the following style:

ts
function doSomething() {
doA();
doB();
doC();
function doA() {}
function doB() {}
function doC() {}
}
ts
function doSomething() {
doA();
doB();
doC();
function doA() {}
function doB() {}
function doC() {}
}

This rule has known false negatives:

ts
function foo() {
console.log(x);
}
foo(); // Should not work
const x = 1;
ts
function foo() {
console.log(x);
}
foo(); // Should not work
const x = 1;

no-useless-rename

  • Severity: error

Don't rename a variable to the same name in import, export, and destructuring.

one-var

  • Severity: off

Generally, you should put each variable declaration on its own line. However, when it makes sense (for example, multiple variables used for very similar purposes: let start = 0, end = 0;), you are free to declare multiple variables consecutively.

prefer-const

  • Severity: error
  • Configuration:
    • Require const as long as any of the destructured variables should be const (destructuring: "any")
    • Do not ignore variables that are only assigned once and read before assignment (ignoreReadBeforeAssign: false)

Only use let when the variable is actually reassigned. Otherwise, use const, which makes TypeScript infer narrower types, and makes the type of each variable easier to trace.

In destructuring, we require using const when any of the variables should be const. Otherwise, this may lead to spillover writability. If you want to make some of the variables let, you should destructure them separately.

ts
const result = doSomething();
const { a, b } = result;
let { c, d } = result;
ts
const result = doSomething();
const { a, b } = result;
let { c, d } = result;

prefer-destructuring

  • Severity: error
  • Configuration:
    • Require destructuring for arrays (array: true)
    • Require destructuring for objects (object: true)
    • Do not require destructuring when the variable is renamed (enforceForRenamedProperties: false)

Destructuring is generally preferred over accessing properties directly. It makes the code more concise and easier to read. There are some catches:

  1. When you are accessing a high array index (for example, const char = str[5]), you may not want to use destructuring like const [, , , , , char] = str. Disable the rule in this case.
  2. In performance-critical cases, array destructuring is slower than property access. const { 0: x } = a may be faster than const [x] = a. This does not matter in general.
  3. Not all index accesses can be safely refactored to array destructuring, unless the object is also iterable. You should use your own discretion when fixing the error.

vars-on-top

  • Severity: error

We don't usually allow vars. When you do use them, put them at the top level of functions/scripts to minimize its quirks.

Naming conventions

camelcase

  • Severity: error
  • Configuration:
    • Require destructured variables to be camelCase (ignoreDestructuring: false)
    • Require global variables to be camelCase (ignoreGlobals: false)
    • Require imported variables to be camelCase (ignoreImports: false)
    • Ignore property names in object literals (properties: "never")

Until we fully use @typescript-eslint/naming-convention, we will still use this rule to enforce camelCase where possible. We don't check object properties because the object may be passed to a third-party library:

ts
checkESLint({
config: {
camel_case: true,
},
});
ts
checkESLint({
config: {
camel_case: true,
},
});

id-denylist

  • Severity: off

You may want to configure this yourself if you want to ban certain identifiers.

id-length

  • Severity: off

We don't think length is a good metric for name quality.

id-match

  • Severity: off

This rule is fully covered by @typescript-eslint/naming-convention.

no-shadow-restricted-names

  • Severity: error

Don't declare a binding called undefined, NaN, Infinity, eval, or arguments. You know exactly what values they represent.

no-underscore-dangle

  • Severity: off

You should generally avoid underscored names and prefer proper encapsulation instead (such as through closures and private names). Do not use underscores to represent throwaway names; just leave it unused (such cases include object destructuring to throw the property away, or function parameters). However, there are cases where you have to use underscores, such as when the name is expected by an external API.

Globals

no-alert

  • Severity: error

There is no good reason to use alert/confirm/prompt in production. They are blocking and look too much like system dialogs.

no-console

  • Severity: can be enabled

console.log is commonly left as debugging artifacts and can occasionally disrupt the console log formatting. For example, Webpack has the unified logger interface for emitting messages. Projects are encouraged to encapsulate their own logger instance as well for unified message formatting and semantics.

However, in more casual projects without a wrapped logger, using console.log may be intentional. This rule can be overridden in user-land.

no-eval

  • Severity: error
  • Configuration:
    • Do not allow indirect eval (allowIndirect: false)

There is not much reason you should use eval—many safe alternatives exist. Indirect eval in strict mode tends to be safe but is still frowned upon. In case you really need to dynamically evaluate code, use new Function instead, which also allows injecting variables via parameters.

no-global-assign

  • Severity: off

We have to turn this rule off, because we cannot make ESLint aware of every global, so the reports are too inconsistent and unhelpful. However, you should know from your heart to only reassign variables in your scope. If you want to modify globals, use globalThis.x instead to make your intention explicit.

no-implied-eval

  • Severity: error

There is no good reason to use setTimeout/setInterval with a string argument. Use a function instead.

no-iterator

  • Severity: error

Don't use the __iterator__ property. No one implements it.

no-new-func

  • Severity: error

You should generally avoid using the Function constructor, because it is just another form of dynamic evaluation. However, compared to eval, it is easier to be used safely, and in case when you need to dynamically evaluate code, you should prefer to use this instead of eval.

no-new-native-nonconstructor

  • Severity: error
  • Related:
    • ts(7009): 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.

Don't construct Symbol and BigInt because they are not meant for construction.

no-new-wrappers

  • Severity: error

Don't construct String, Number, and Boolean objects because they are much harder to use and do not have any benefits over primitives.

no-obj-calls

  • Severity: error
  • Related:
    • ts(2349): This expression is not callable. Type 'Math' has no call signatures.

Don't call Math, JSON, and other namespaces.

no-object-constructor

  • Severity: error

Don't use new Object() because it's just a longer way of writing {}. Always use Object with an argument.

no-proto

  • Severity: error

Don't access the __proto__ property. Use Object.getPrototypeOf and Object.setPrototypeOf instead. Note that the __proto__ syntax in object literals is still allowed and should be preferred over Object.create.

ts
// Write this:
const obj = {
__proto__: null,
};
// Instead of this:
const obj = Object.create(null);
ts
// Write this:
const obj = {
__proto__: null,
};
// Instead of this:
const obj = Object.create(null);
note

TypeScript does not support the __proto__ syntax in object literals yet. However, Object.create will be always typed as any, so the former should still be preferred. Cast with null as never when necessary.

no-prototype-builtins

  • Severity: error

Don't use Object.prototype methods because they are not safe against null and undefined. Generally, you don't need to jump hoops to get similar behavior.

ts
// Instead of:
foo.hasOwnProperty("bar");
foo.propertyIsEnumerable("bar");
foo.isPrototypeOf(bar);
// Write:
Object.hasOwn(foo, "bar");
Object.getOwnPropertyDescriptor(foo, "bar")?.enumerable;
Object.prototype.isPrototypeOf.call(foo, bar); // Do you really need this?
ts
// Instead of:
foo.hasOwnProperty("bar");
foo.propertyIsEnumerable("bar");
foo.isPrototypeOf(bar);
// Write:
Object.hasOwn(foo, "bar");
Object.getOwnPropertyDescriptor(foo, "bar")?.enumerable;
Object.prototype.isPrototypeOf.call(foo, bar); // Do you really need this?

no-restricted-properties

  • Severity: off

You may want to configure this yourself if you want to ban certain identifiers or certain properties.

no-undef

  • Severity: error
  • Configuration:
    • Disallow using typeof on undefined variables (typeof: true)
  • Related:
    • ts(2304): Cannot find name 'a'.

Don't use undefined variables. If you want to check if a variable is defined, use if ("x" in globalThis) or if (typeof globalThis.x !== "undefined").

Note that this rule is only useful in a plain-JS project. In a TypeScript project, you should use TypeScript checks instead. You may find cases where ESLint is unaware of a global variable. In this case, either change your env setting, or use globalThis.x.

no-undefined

  • Severity: off

There's virtually no risk to use undefined nowadays, especially with rules like no-shadow-restricted-names. Furthermore, because undefined is so pervasive as the implicit "value of absence", it's hard to avoid it. You should generally use undefined instead of null as the default value, unless the latter has a semantic difference from undefined.

prefer-object-has-own

  • Severity: error

Use Object.hasOwn instead of Object.prototype.hasOwnProperty.call. It's shorter and more readable. If you need compatibility, install a polyfill. (You should never use x.hasOwnProperty, by the way; see no-prototype-builtins.)