Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
docs: blog post on Avoidingany
s with Linting and TypeScript#10362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Merged
JoshuaKGoldberg merged 27 commits intotypescript-eslint:mainfromJoshuaKGoldberg:blog-post-catching-anyJan 21, 2025
Uh oh!
There was an error while loading.Please reload this page.
Merged
Changes fromall commits
Commits
Show all changes
27 commits Select commitHold shift + click to select a range
85d4fa4
[WIP] docs: blog post on catching any
JoshuaKGoldberg5475369
Seems legit
JoshuaKGoldberg6b8c357
Merge branch 'main' into blog-post-catching-any
JoshuaKGoldberg162b3cc
Update packages/website/blog/2025-01-13-catching-any.mdx
JoshuaKGoldberga33dc85
Apply suggestions from code review
JoshuaKGoldberg3da18e7
Apply suggestions from code review
JoshuaKGoldberg8450d97
A bit of intro rephrasing
JoshuaKGoldberg5862c33
no-unsafe-call block
JoshuaKGoldberg0b1541c
Merge branch 'main' into blog-post-catching-any
JoshuaKGoldberge62fb94
Rename to 'Avoiding', and better truncate
JoshuaKGoldbergb1ee81d
Pluralize slug
JoshuaKGoldberg8546630
Merge branch 'main' into blog-post-catching-any
JoshuaKGoldberg8e0cd0b
add anys to cspell.json words
JoshuaKGoldberg55726f5
404 fixes: catching-any -> avoiding-any
JoshuaKGoldberg0447cb0
title fixes: catching-any -> avoiding-any
JoshuaKGoldberg4acb807
Final slug fixes: avoiding-any -> avoiding-anys
JoshuaKGoldbergddb88ab
Apply suggestions from code review
JoshuaKGoldberg2c61661
More improvements
JoshuaKGoldberga985afd
Agreed, 'absolutely' is a bit much
JoshuaKGoldbergea11fa7
around any safety
JoshuaKGoldberg0e2faf3
It can either be a deep dive or an overview, not both
JoshuaKGoldberge6f5af9
It's a blog post; add back no-throw
JoshuaKGoldberg1bb13d6
Bit of an opening reword, again
JoshuaKGoldberg4c21a9b
sort footnotes
JoshuaKGoldberg8f77f28
more process
kirkwaiblinger97433d6
Update packages/website/blog/2025-01-19-avoiding-anys.mdx
JoshuaKGoldberg6ed0cb0
Schedule post for the 21st
JoshuaKGoldbergFile filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
1 change: 1 addition & 0 deletions.cspell.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -55,6 +55,7 @@ | ||
"allowforknownsafepromises", | ||
"ambiently", | ||
"Amiel", | ||
"anys", | ||
"Arka", | ||
"Armano", | ||
"astexplorer", | ||
1 change: 1 addition & 0 deletionspackages/eslint-plugin/docs/rules/no-explicit-any.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletionspackages/eslint-plugin/docs/rules/no-unsafe-argument.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletionspackages/eslint-plugin/docs/rules/no-unsafe-assignment.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletionspackages/eslint-plugin/docs/rules/no-unsafe-call.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletionspackages/eslint-plugin/docs/rules/no-unsafe-function-type.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletionspackages/eslint-plugin/docs/rules/no-unsafe-member-access.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletionspackages/eslint-plugin/docs/rules/no-unsafe-return.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletionspackages/eslint-plugin/docs/rules/use-unknown-in-catch-callback-variable.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
288 changes: 288 additions & 0 deletionspackages/website/blog/2025-01-21-avoiding-anys.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
--- | ||
authors: joshuakgoldberg | ||
description: How typescript-eslint expands on TypeScript's type safety to catch explicit and implicit `any`s. | ||
slug: avoiding-anys | ||
tags: [any, no-explicit-any, no-unsafe, noImplicitAny, typed linting] | ||
title: 'Avoiding `any`s with Linting and TypeScript' | ||
--- | ||
import Tabs from '@theme/Tabs'; | ||
import TabItem from '@theme/TabItem'; | ||
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. | ||
{/* truncate */} | ||
## `noImplicitAny` Isn't Enough | ||
TypeScript includes a [`noImplicitAny`](https://www.typescriptlang.org/tsconfig/#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](https://www.typescriptlang.org/tsconfig/#strict) 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](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1), 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`](/rules/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`: | ||
```ts | ||
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`](/rules/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`](/rules/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`: | ||
```ts | ||
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 JavaScript[^no-throws]. | ||
Therefore, TypeScript treats all caught values as `any` by default. | ||
TypeScript's [`useUnknownInCatchVariables`](https://www.typescriptlang.org/tsconfig/#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`](/rules/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: | ||
```ts | ||
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`](/rules/no-unsafe-argument) reports on passing an `any` typed value to a function call | ||
- [`@typescript-eslint/no-unsafe-assignment`](/rules/no-unsafe-assignment) reports on assigning an `any` typed value to a property or variable | ||
- [`@typescript-eslint/no-unsafe-call`](/rules/no-unsafe-call) reports on calling an `any` typed value like a function | ||
- [`@typescript-eslint/no-unsafe-member-access`](/rules/no-unsafe-member-access) reports on accessing members of any value typed as `any` | ||
- [`@typescript-eslint/no-unsafe-return`](/rules/no-unsafe-return) reports on returning a value typed as `any` 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: | ||
```ts | ||
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](https://zod.dev), 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`](/rules/ban-ts-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: | ||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
```ts | ||
import { processString } from '@example/package'; | ||
// @ts-ignore | ||
processString('New York City'); | ||
``` | ||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
```ts | ||
import { processString } from '@example/package'; | ||
// @ts-expect-error -- Pending updating the processString types. See GH-1234. | ||
processString('New York City'); | ||
``` | ||
</TabItem> | ||
</Tabs> | ||
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`](https://eslint-community.github.io/eslint-plugin-eslint-comments). | ||
The plugin's [recommended preset](https://eslint-community.github.io/eslint-plugin-eslint-comments/#%F0%9F%93%96-usage) enables rules that enforce best practices around ESLint comment directives, including: | ||
- [`@eslint-community/eslint-comments/disable-enable-pair`](https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/disable-enable-pair.html): to prevent accidentally disabling rules for the rest of a file, rather than on specific lines | ||
- [`@eslint-community/eslint-comments/no-unlimited-disable`](https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-unlimited-disable.html): to prevent accidentally disabling all ESLint rules, rather than specific ones | ||
- [`@eslint-community/eslint-comments/require-description`](https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/require-description.html): 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: | ||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
```ts | ||
import { processString } from '@example/package'; | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call | ||
processString('New York City'); | ||
``` | ||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
```ts | ||
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'); | ||
``` | ||
</TabItem> | ||
</Tabs> | ||
Once the types for `@example/package` are fixed to no longer produce an `any`, running ESLint with [`reportUnusedDisableDirectives`](https://eslint.org/docs/latest/use/configure/rules#report-unused-eslint-disable-comments) 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()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse), [`.json()`](https://developer.mozilla.org/en-US/docs/Web/API/Response/json), and [`Storage`](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#basic_concepts) properties are all typed as `any` by default in TypeScript. | ||
TypeScript keeps `any` in the types for legacy support reasons[^compiler-option-lib-any]. | ||
The [`ts-reset`](https://www.totaltypescript.com/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`: | ||
```ts | ||
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`](/users/configs#recommended-type-checked) 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`](/users/configs#strict-type-checked) 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. | ||
[^compiler-option-lib-any]: [microsoft/TypeScript#60899 Compiler option to switch lib .d.ts anys to unknown](https://github.com/microsoft/TypeScript/issues/60899) | ||
[^no-throws]: [Why TypeScript Doesn't Include a `throws` Keyword](https://www.learningtypescript.com/articles/why-typescript-doesnt-include-a-throws-keyword) |
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.