Skip to main content

Avoiding anys with Linting and TypeScript

· 9 min read
Josh Goldberg
typescript-eslint Maintainer

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 anys 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 anys 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 anys from popping up. But these lint rules put an immediate restriction on unsafe types -- and help guide towards better alternatives.

Banning Explicit anys

@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 anys.

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.

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.

tip

// @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:

import { processString } from '@example/package';

// @ts-ignore
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:

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:

import { processString } from '@example/package';

// 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 anys 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-incatch-callback-variable and other rules that enforce more strict type safety and TypeScript best practices.

Footnotes

  1. Why TypeScript Doesn't Include a throws Keyword

  2. microsoft/TypeScript#60899 Compiler option to switch lib .d.ts anys to unknown