visit
TypeScript claims to be a strongly typed programming language built on top of JavaScript, providing better tooling at any scale. However, TypeScript includes the any
type, which can often sneak into a codebase implicitly and lead to a loss of many of TypeScript's advantages.
This article explores ways to take control of the any
type in TypeScript projects. Get ready to unleash the power of TypeScript, achieving ultimate type safety and improving code quality.
However, as soon as you start using the any
type in your codebase, you lose all the benefits listed above. The any
type is a dangerous loophole in the type system, and using it disables all type-checking capabilities as well as all tooling that depends on type-checking. As a result, all the benefits of TypeScript are lost: bugs are missed, code editors become less useful, and more.
function parse(data: any) {
return data.split('');
}
// Case 1
const res1 = parse(42);
// ^ TypeError: data.split is not a function
// Case 2
const res2 = parse('hello');
// ^ any
parse
function. When you type data.
in your editor, you won't be given correct suggestions for the available methods for data
.TypeError: data.split is not a function
error because we passed a number instead of a string. TypeScript is not able to highlight the error because any
disables type checking.res2
variable also has the any
type. This means that a single usage of any
can have a cascading effect on a large portion of a codebase.
Using any
is okay only in extreme cases or for prototyping needs. In general, it is better to avoid using any
to get the most out of TypeScript.
It's important to be aware of the sources of the any
type in a codebase because explicitly writing any
is not the only option. Despite our best efforts to avoid using the any
type, it can sometimes sneak into a codebase implicitly.
There are four main sources of the any
type in a codebase:
any
in a codebase.
I have already written articles on Key Considerations in tsconfig and Improving Standard Library Types for the first two points. Please check them out if you want to improve type safety in your projects.
This time, we will focus on automatic tools for controlling the appearance of the any
type in a codebase.
The most common configuration for typescript-eslint
is as follows:
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
plugins: ['@typescript-eslint'],
parser: '@typescript-eslint/parser',
root: true,
};
This configuration enables eslint
to understand TypeScript at the syntax level, allowing you to write simple eslint rules that apply to manually written types in a code. For example, you can forbid the explicit use of any
.
The recommended
preset contains a carefully selected set of ESLint rules aimed at improving code correctness. While it's recommended to use the entire preset, for the purpose of this article, we will focus only on the no-explicit-any
rule.
TypeScript's strict mode prevents the use of implied any
, but it doesn't prevent any
from being explicitly used. The no-explicit-any
rule helps to prohibit manually writing any
anywhere in a codebase.
// ❌ Incorrect
function loadPokemons(): any {}
// ✅ Correct
function loadPokemons(): unknown {}
// ❌ Incorrect
function parsePokemons(data: Response<any>): Array<Pokemon> {}
// ✅ Correct
function parsePokemons(data: Response<unknown>): Array<Pokemon> {}
// ❌ Incorrect
function reverse<T extends Array<any>>(array: T): T {}
// ✅ Correct
function reverse<T extends Array<unknown>>(array: T): T {}
The primary purpose of this rule is to prevent the use of any
throughout the team. This is a means of strengthening the team's agreement that the use of any
in the project is discouraged.
This is a crucial goal because even a single use of any
can have a cascading impact on a significant portion of the codebase due to . However, this is still far from achieving ultimate type safety.
Although we have dealt with explicitly used any
, there are still many implied any
within a project's dependencies, including npm packages and TypeScript's standard library.
const response = await fetch('//pokeapi.co/api/v2/pokemon');
const pokemons = await response.json();
// ^? any
const settings = JSON.parse(localStorage.getItem('user-settings'));
// ^? any
Both variables pokemons
and settings
were implicitly given the any
type. Neither no-explicit-any
nor TypeScript's strict mode will warn us in this case. Not yet.
This happens because the types for response.json()
and JSON.parse()
come from TypeScript's standard library, where these methods have an explicit any
annotation. We can still manually specify a better type for our variables, but there are nearly 1,200 occurrences of any
in the standard library. It's nearly impossible to remember all the cases where any
can sneak into our codebase from the standard library.
The same goes for external dependencies. There are many poorly typed libraries in npm, with most still being written in JavaScript. As a result, using such libraries can easily lead to a lot of implicit any
in a codebase.
Generally, there are still many ways for any
to sneak into our code.
Ideally, we would like to have a setting in TypeScript that makes the compiler complain about any variable that has received the any
type for any reason. Unfortunately, such a setting does not currently exist and is not expected to be added.
We can achieve this behavior by using the type-checked mode of the typescript-eslint
plugin. This mode works in conjunction with TypeScript to provide complete type information from the TypeScript compiler to ESLint rules. With this information, it is possible to write more complex ESLint rules that essentially extend the type-checking capabilities of TypeScript. For instance, a rule can find all variables with the any
type, regardless of how any
was obtained.
module.exports = {
extends: [
'eslint:recommended',
- 'plugin:@typescript-eslint/recommended',
+ 'plugin:@typescript-eslint/recommended-type-checked',
],
plugins: ['@typescript-eslint'],
parser: '@typescript-eslint/parser',
+ parserOptions: {
+ project: true,
+ tsconfigRootDir: __dirname,
+ },
root: true,
};
To enable type inference for typescript-eslint
, add parserOptions
to ESLint configuration. Then, replace the recommended
preset with recommended-type-checked
. The latter preset adds about 17 new powerful rules. For the purpose of this article, we will focus on only 5 of them.
The no-unsafe-argument
rule searches for function calls in which a variable of type any
is passed as a parameter. When this happens, type-checking is lost, and all the benefits of strong typing are also lost.
For example, let's consider a saveForm
function that requires an object as a parameter. Suppose we receive JSON, parse it, and obtain an any
type.
// ❌ Incorrect
function saveForm(values: FormValues) {
console.log(values);
}
const formValues = JSON.parse(userInput);
// ^? any
saveForm(formValues);
// ^ Unsafe argument of type `any` assigned
// to a parameter of type `FormValues`.
When we call the saveForm
function with this parameter, the no-unsafe-argument
rule flags it as unsafe and requires us to specify the appropriate type for the value
variable.
// ❌ Incorrect
saveForm({
name: 'John',
address: JSON.parse(addressJson),
// ^ Unsafe assignment of an `any` value.
});
The best way to fix the error is to use TypeScript’s or a validation library such as or . For instance, let's write the parseFormValues
function that narrows the precise type of parsed data.
// ✅ Correct
function parseFormValues(data: unknown): FormValues {
if (
typeof data === 'object' &&
data !== null &&
'name' in data &&
typeof data['name'] === 'string' &&
'address' in data &&
typeof data.address === 'string'
) {
const { name, address } = data;
return { name, address };
}
throw new Error('Failed to parse form values');
}
const formValues = parseFormValues(JSON.parse(userInput));
// ^? FormValues
saveForm(formValues);
Note that it is allowed to pass the any
type as an argument to a function that accepts unknown
, as there are no safety concerns associated with doing so.
// ✅ Correct
import { z } from 'zod';
const schema = z.object({
name: z.string(),
address: z.string(),
});
const formValues = schema.parse(JSON.parse(userInput));
// ^? { name: string, address: string }
saveForm(formValues);
The no-unsafe-assignment
rule searches for variable assignments in which a value has the any
type. Such assignments can mislead the compiler into thinking that a variable has a certain type, while the data may actually have a different type.
// ❌ Incorrect
const formValues = JSON.parse(userInput);
// ^ Unsafe assignment of an `any` value
Thanks to the no-unsafe-assignment
rule, we can catch the any
type even before passing formValues
elsewhere. The fixing strategy remains the same: We can use type narrowing to provide a specific type to the variable's value.
// ✅ Correct
const formValues = parseFormValues(JSON.parse(userInput));
// ^? FormValues
The no-unsafe-member-access
rule prevents us from accessing object properties if a variable has the any
type, since it may be null
or undefined
.
The no-unsafe-call
rule prevents us from calling a variable with the any
type as a function, as it may not be a function.
Let's imagine that we have a poorly typed third-party library called untyped-auth
:
// ❌ Incorrect
import { authenticate } from 'untyped-auth';
// ^? any
const userInfo = authenticate();
// ^? any ^ Unsafe call of an `any` typed value.
console.log(userInfo.name);
// ^ Unsafe member access .name on an `any` value.
authenticate
function can be unsafe, as we may forget to pass important arguments to the function.name
property from the userInfo
object is unsafe, as it will be null
if authentication fails.
// ✅ Correct
import { authenticate } from 'untyped-auth';
// ^? (login: string, password: string) => Promise<UserInfo | null>
const userInfo = await authenticate('test', 'pwd');
// ^? UserInfo | null
if (userInfo) {
console.log(userInfo.name);
}
The no-unsafe-return
rule helps to not accidentally return the any
type from a function that should return something more specific. Such cases can mislead the compiler into thinking that a returned value has a certain type, while the data may actually have a different type.
// ❌ Incorrect
interface FormValues {
name: string;
address: string;
}
function parseForm(json: string): FormValues {
return JSON.parse(json);
// ^ Unsafe return of an `any` typed value.
}
const form = parseForm('null');
console.log(form.name);
// ^ TypeError: Cannot read properties of null
The parseForm
function may lead to runtime errors in any part of the program where it is used, since the parsed value is not checked. The no-unsafe-return
rule prevents such runtime issues.
// ✅ Correct
import { z } from 'zod';
const schema = z.object({
name: z.string(),
address: z.string(),
});
function parseForm(json: string): FormValues {
return schema.parse(JSON.parse(json));
}
It is worth noting that just inferring the types works faster than the usual invocation of the tsc
compiler. For example, on our most recent project with about 1.5 million lines of TypeScript code, type checking through tsc
takes about 11 minutes, while the additional time required for ESLint's type-aware rules to bootstrap is only about 2 minutes.
Controlling the use of any
in TypeScript projects is crucial for achieving optimal type safety and code quality. By utilizing the typescript-eslint
plugin, developers can identify and eliminate any occurrences of the any
type in their codebase, resulting in a more robust and maintainable codebase.
By using type-aware eslint rules, any appearance of the keyword any
in our codebase will be a deliberate decision rather than a mistake or oversight. This approach safeguards us from using any
in our own code, as well as in the standard library and third-party dependencies.