Avoiding any
s with Linting and TypeScript
TypeScript's any
type is, by design, the single most unsafe part of its type system.
The any
type indicates a type that can be anything and can be used in place of any other type.
Using any
is unsafe because the type disables many of TypeScript's type-checking features and hampers TypeScript's ability to provide developer assistance.
typescript-eslint includes several lint rules that help prevent unsafe practices around the any
type.
These rules flag uses of any
or code patterns that sneakily introduce it.
In this blog post, we'll show you those lint rules and several other handy ways to prevent any
s from sneaking into your code.
noImplicitAny
Isn't Enough
TypeScript includes a noImplicitAny
flag to report when a better type than any
can't be inferred for a value.
noImplicitAny
is part of its family of strict
compiler options and is generally recommended for all projects.
However, even with noImplicitAny
enabled, the any
type can easily be introduced into codebases.
noImplicitAny
doesn't prevent developers from explicitly writing the any
type in type type annotations.
Some built-in APIs such as JSON.stringify
and types such as Function
can introduce the any
type without triggering noImplicitAny
.
Enabling noImplicitAny
is a great first step towards better project type safety, but it's not enough to prevent any
s altogether.
Banning Unsafe Types
The first line of defense against any
for many repositories is adding lint rules that report on explicitly written unsafe types.
Developers can always disable ESLint rules with inline comments, so this isn't guaranteed to prevent explicit any
s from popping up.
But these lint rules put an immediate restriction on unsafe types -- and help guide towards better alternatives.
Banning Explicit any
s
@typescript-eslint/no-explicit-any
reports on any instance of the any
type written in your source code.
Doing so helps prevent developers from using any
instead of a more safe type.
Take the following unsafe declaration of friend: any
in a greet
function.
Because its friend
parameter is typed as any
instead of string
, TypeScript won't report a type error on a call that provides another type.
@typescript-eslint/no-explicit-any
would report on that any
:
function greet(friend: any) {
// ~~~
// eslint(@typescript-eslint/no-explicit-any):
// Unexpected any. Specify a different type.
console.log(`Hello, ${friend.toUpperCase()}!`);
}
greet('Lazlo'); // Ok
greet({ name: 'Nadya' }); // Should be a type error, but isn't
Instead of any
, it would have been more type safe to give friend
the more precise type string
.
Prefer unknown
If you have data that is of an unknown type, instead of describing it with the unsafe any
type, prefer the safer unknown
instead.
unknown
is almost always preferable because it doesn't allow using the value in any arbitrary, potentially unsafe way.
@typescript-eslint/no-explicit-any
's rule reports include a suggestion fixer that switches the explicit any
to unknown
.
Banning Function
@typescript-eslint/no-unsafe-function-type
reports on any instance of the built-in Function
type written in source code.
Function
is a loose, unsafe type: it allows being called with any number of arguments and returns type any
.
Take the following version of greet
that takes in a function for its parameter.
Function
doesn't describe any parameter or return types, so TypeScript can't know what types it's meant to take in or return.
@typescript-eslint/no-unsafe-function-type
would report on that Function
:
function greet(getFriend: Function) {
// ~~~~~~~~
// eslint(@typescript-eslint/no-unsafe-function-type):
// The `Function` type accepts any function-like value.
// Prefer explicitly defining any function parameters and return type.
console.log(`Hello, ${getFriend().toUpperCase()}!`);
}
greet(() => 'Lazlo'); // Ok
greet(() => ({ name: 'Nadya' })); // Should be a type error, but isn't
Instead of Function
, it would have been more type safe to give getFriend
the more precise type () => string
.
Enforcing unknown
for Caught Exceptions
There is no way to indicate what types functions may throw in TypeScript, due to the highly dynamic nature of JavaScript1.
Therefore, TypeScript treats all caught values as any
by default.
TypeScript's useUnknownInCatchVariables
changes catch
block variables to have the more appropriate unknown
type, but no compiler option equivalent exists for Promise
rejections' .catch()
method.
@typescript-eslint/use-unknown-in-catch-callback-variable
enforces always using the unknown
type for the parameter of a Promise rejection callback.
For example, given the following code, TypeScript would not report a type error, but the lint rule would report:
function rejectWith(value: string) {
return Promise.reject(value);
}
rejectWith('Nandor').catch(error => {
// ~~~~~
// eslint(@typescript-eslint/use-unknown-in-catch-callback-variable):
// Prefer the safe `: unknown` for a `catch` callback variable.
console.log(error.message); // Should be a type error, but isn't
});
Had the error
been given a : unknown
type, TypeScript would be able to report on error.message
as being unsafe.
Banning Usage of Unsafe Types
Once an any
type exists in code, it is "infectious": it can turn the types of values based on it into more any
s.
typescript-eslint includes a collection of rules that flag code patterns which make use of any
's unsafe, viral nature.
Each of the following rules reports on a specific use of any
.
@typescript-eslint/no-unsafe-argument
reports on passing anany
typed value to a function call@typescript-eslint/no-unsafe-assignment
reports on assigning anany
typed value to a property or variable@typescript-eslint/no-unsafe-call
reports on calling anany
typed value like a function@typescript-eslint/no-unsafe-member-access
reports on accessing members of any value typed asany
@typescript-eslint/no-unsafe-return
reports on returning a value typed asany
from a function
For example, this parseData
function violates several of those lint rules by creating a shape
value of type any
and not validating its type before using it:
export interface Shape {
label: string;
value: number;
}
export function parseShapeFromData(raw: string): Shape {
const shape = JSON.parse(raw);
// ~~~~~~~~~~~~~~~~~~~~~~~
// eslint(@typescript-eslint/no-unsafe-assignment):
// Unsafe assignment of an `any` value.
console.log('Making a shape with value:', shape.value);
// ~~~~~
// eslint(@typescript-eslint/no-unsafe-member-access):
// Unsafe member access .value of an `any` value.
return shape;
// eslint(@typescript-eslint/no-unsafe-return):
// Unsafe return of an `any` value.
}
Had the parseShapeFromData
function included checks on the type of the shape
value or used a schema validation library such as Zod, it would be informed at runtime if its raw
string didn't create the expected Shape
.
Put together, the @typescript-eslint/no-unusafe-*
rules around any
safety provide a comprehensive suite of checks that can flag most accidental uses of any
across a codebase.
We highly recommend using them alongside TypeScript's noImplicitAny
compiler option.
Additional Helpers
Disabling TypeScript Suppressions
The any
type is not the only way to bypass TypeScript's type system.
Developers are also able to use inline TypeScript directives: // @ts-expect-error
and // @ts-ignore
.
Those inline comment directives cause TypeScript to ignore type errors on a line.
Disabling TypeScript on a per-line basis can sometimes be necessary in rare cases, but is always unsafe and should not be part of your standard toolkit.
// @ts-expect-error
and // @ts-ignore
are almost the same, except // @ts-expect-error
will produce a new error if there isn't any existing error for it to suppress.
It's generally preferable to use // @ts-expect-error
over // @ts-ignore
.
@typescript-eslint/ban-comment
can be used to report on cases where developers use TypeScript comment directives.
By default, the rule:
- Prohibits all
@ts-ignore
and@ts-nocheck
comment directives - Prohibits
@ts-expect-error
directives unless they have an explanatory comment description - Requires explanatory comment descriptions to contain at least 3 characters
For example, suppose "@example/package"
exports a processString
function that should take in a string
but whose types incorrectly indicate take in a number
.
A // @ts-expect-error
comment could be used to tell TypeScript to ignore the line:
- ❌ Incorrect
- ✅ Correct
import { processString } from '@example/package';
// @ts-ignore
processString('New York City');
import { processString } from '@example/package';
// @ts-expect-error -- Pending updating the processString types. See GH-1234.
processString('New York City');
Once the types for @example/package
are fixed to no longer produce a type error, the comment directive will itself produce a type error asking to delete itself.
ESLint Comments Plugin
ESLint includes its own inline comment directives separate from TypeScript's.
// eslint-disable
, // eslint-disable-next-line
, and other inline ESLint config comments can suppress lint rules — including those mentioned in this post.
Suppressing ESLint rules can also be necessary in cases where the rule and/or TypeScript's type system have misunderstand your code.
If you must use ESLint comment directives, we recommend utilizing @eslint-community/eslint-plugin-eslint-comments
.
The plugin's recommended preset enables rules that enforce best practices around ESLint comment directives, including:
@eslint-community/eslint-comments/disable-enable-pair
: to prevent accidentally disabling rules for the rest of a file, rather than on specific lines@eslint-community/eslint-comments/no-unlimited-disable
: to prevent accidentally disabling all ESLint rules, rather than specific ones@eslint-community/eslint-comments/require-description
: to require comments explaining why directives are necessary
For example, suppose the type of an imported variable is temporarily any
pending some soon-to-be-completed refactoring.
It might be reasonable to include an ESLint disable comment to suppress a lint rule reporting on the any
.
That comment should ideally include an explanation and link to the pending task, so developers know it's not normal to disable lint rules without good reason:
- ❌ Incorrect
- ✅ Correct
import { processString } from '@example/package';
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
processString('New York City');
import { processString } from '@example/package';
// Pending GH-1234: will soon not be any.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
processString('New York City');
Once the types for @example/package
are fixed to no longer produce an any
, running ESLint with reportUnusedDisableDirectives
option will produce a lint report asking to remove the comment directive.
ts-reset
Some sources of any
come from built-in global types provided by TypeScript.
JSON.parse()
, .json()
, and Storage
properties are all typed as any
by default in TypeScript.
TypeScript keeps any
in the types for legacy support reasons2.
The ts-reset
library switches those global type definitions to safer equivalents.
It switches the any
s in those built-in types to unknown
.
With ts-reset
, the following data
variable would switch from being type any
to unknown
:
const data = JSON.parse(`"clearly-a-string"`);
// ^? any (without ts-reset)
// ^? unknown (with ts-reset)
console.log(data.some.property.that.does.not.exist);
Note that ts-reset
applies globally, so it should only be used in application code.
Next Steps
We highly recommend using at least the tseslint.configs.recommendedTypeChecked
preset in your ESLint configuration.
It enables the no-explicit-any
and no-unsafe-*
lint rules mentioned in this blog post, as well as a large set of other rules that help enforce type safety and TypeScript best practices.
If you're interested in achieving stronger type safety with more strict linting, consider upgrading to the tseslint.configs.strictTypeChecked
preset.
It includes all the recommended rules, as well as use-unknown-in-catch-callback-variable
and other rules that enforce more strict type safety and TypeScript best practices.