-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
docs: add 'Typed Linting' blog post #10025
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
JoshuaKGoldberg
merged 9 commits into
typescript-eslint:main
from
JoshuaKGoldberg:blog-typed-linting
Sep 30, 2024
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
e32fb0d
docs: add 'Typed Linting' blog post
JoshuaKGoldberg 04ce78e
fix: /getting-started/linting-with-type-information
JoshuaKGoldberg 09d615b
fix tags
JoshuaKGoldberg 2701669
Maybe better explain type information...
JoshuaKGoldberg e1be9f1
more touchups
JoshuaKGoldberg dfdb1a5
tuples
JoshuaKGoldberg ca79a79
nit: traditional 'JS' rules
JoshuaKGoldberg 88f153d
Apply suggestions from code review
JoshuaKGoldberg 3f29422
mention: reliable
JoshuaKGoldberg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
|
@@ -83,6 +83,7 @@ | |
"Cheung", | ||
"codebases", | ||
"Codecov", | ||
"codemod", | ||
"contravariant", | ||
"Crockford", | ||
"declarators", | ||
|
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
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,258 @@ | ||
--- | ||
authors: joshuakgoldberg | ||
description: Explaining what linting with type information means, why it's so powerful, and some of the useful rules you can enable that use it. | ||
slug: typed-linting | ||
tags: [types, type information, typed linting] | ||
title: 'Typed Linting: The Most Powerful TypeScript Linting Ever' | ||
--- | ||
|
||
[Linting with type information](https://typescript-eslint.io/getting-started/typed-linting), also called "typed linting" or "type-aware linting", is the act of writing lint rules that use type information to understand your code. | ||
Typed linting rules as provided by typescript-eslint are the most powerful JavaScript/TypeScript linting in common use today. | ||
|
||
In this blog post, we'll give a high-level overview of how linting with type information works, why it's so much more powerful than traditional linting, and some of the useful rules you can enable that use it. | ||
|
||
<!--truncate--> | ||
|
||
## Recap: Type Information? | ||
|
||
Traditional JavaScript lint rules operate on one file at a time. | ||
They look at a description of code in each file and report complaints if that file seems to contain bad practices. | ||
That description is called an Abstract Syntax Tree, or AST. | ||
|
||
:::tip | ||
For a primer on ASTs and linting, see _[ASTs and typescript-eslint](./2022-12-05-asts-and-typescript-eslint.md)_. | ||
::: | ||
|
||
Each file's AST contains only information for that file, not any other files. | ||
Lint rules that rely only on the file's AST don't have a way to understand code imported from other files, such as in ESM `import` statements. | ||
Not being able to understand code from other files severely limits lint rules. | ||
|
||
As an example, suppose you enable a lint rule like [`@typescript-eslint/no-deprecated`](/rules/no-deprecated) to prevent calling to code with a `@deprecated` JSDoc. | ||
Using just the following file's AST, the lint rule would have no way of knowing whether `work` is deprecated: | ||
|
||
```ts title="index.ts" | ||
import { work } from './worker'; | ||
|
||
// Is this safe? Does calling work violate any rules? We don't know! | ||
work(); | ||
``` | ||
|
||
_Type information_ refers to the information a type checker such as TypeScript generates to understand your code. | ||
Type checkers read code, determine what types each value may be, and store that "type information". | ||
TypeScript and tools that call to TypeScript's APIs can then use that type information to understand the project's code. | ||
|
||
In the earlier example, type information would be able to inform a lint rule running in `index.ts` that the `work` import resolves to a function in another file: | ||
|
||
```ts title="worker.ts" | ||
/** @deprecated - Don't do this! */ | ||
export function work() { | ||
// ... | ||
} | ||
``` | ||
|
||
...which would allow the lint rule to report a complaint that the `work()` call is to a function marked as `@deprecated`. | ||
|
||
typescript-eslint allows lint rules to retrieve type information using TypeScript's APIs. | ||
In doing so, they can even make decisions on linted files using information outside each individual file. | ||
|
||
## Common Uses for Typed Linting | ||
|
||
Cross-file type information is a powerful addition to lint rules. | ||
Knowing the types of pieces of your code allows lint rules to flag for risky behavior specific to certain types. | ||
The following sections show several of the most common uses for lint rules that rely on type information. | ||
|
||
### Unsafe `any`s | ||
|
||
The `@typescript-eslint/no-unsafe-*` family of rules checks for risky uses of `any` typed values. | ||
This is useful because the `any` type can easily slip into code and reduce type safety, despite being allowed by the TypeScript type checker. | ||
|
||
For example, the following code that logs a member of an object parsed from a string produces no type errors in type checking. | ||
`JSON.parse()` returns `any`, and arbitrary property accesses are allowed on values of type `any`. | ||
|
||
However, [`@typescript-eslint/no-unsafe-member-access`](/rules/no-unsafe-member-access) would report `[key]` might not be a property on the object: | ||
|
||
```ts | ||
function getDataKey(rawData: string, key: string): string { | ||
return JSON.parse(rawData)[key]; | ||
// ~~~~~ | ||
// Unsafe member access [key] on an `any` value. | ||
// eslint(@typescript-eslint/no-unsafe-member-access) | ||
} | ||
``` | ||
|
||
The lint rule is right to report. | ||
Calls to the `getDataKey` function can return a value that's not a `string`, despite the function's explicit return type annotation. | ||
That can lead to unexpected behavior at runtime: | ||
|
||
```ts | ||
console.log(getDataKey(`{ "blue": "cheese" }`, 'bleu').toUpperCase()); | ||
// Uncaught TypeError: Cannot read properties of undefined (reading 'toUpperCase') | ||
``` | ||
|
||
Without type information to indicate the types of `JSON.parse` and `key`, there would have been no way to determine that the `[key]` member access was unsafe. | ||
|
||
### Method Call Scoping | ||
|
||
<!-- - Catching [dangerous calls to unbound methods that fail to provide a `this`](/rules/no-unbound-method) --> | ||
|
||
Runtime crashes caused by misuses of typed code are possible even with no `any`s. | ||
|
||
For example, class method functions don't preserve their class scope when passed as standalone variables ("unbound"). | ||
TypeScript still allows them to be called without the proper `this` scope. | ||
|
||
The global `localStorage` object in browsers has several properties that must be called with a `this` bound to `localStorage`. | ||
The [`@typescript-eslint/unbound-method`](/rules/unbound-method) lint rule can report on unsafe references to those properties, such as accessing `getItem`: | ||
|
||
```ts | ||
const { getItem } = localStorage; | ||
// ~~~~~~~ | ||
// Avoid referencing unbound methods which may cause unintentional scoping of `this`. | ||
// eslint(@typescript-eslint/unbound-method) | ||
``` | ||
|
||
That's useful because calls to `getItem` that aren't bound to `localStorage` cause an exception at runtime: | ||
|
||
```ts | ||
getItem('...'); | ||
// Uncaught TypeError: Illegal invocation | ||
``` | ||
|
||
Without type information to indicate the types of `localStorage` and its `getItem` property, there would have been no reliable way to determine that the `const { getItem }` access was unsafe. | ||
|
||
### Async Race Conditions | ||
|
||
Even if your code is 100% typed, has no `any`s, and doesn't misuse scopes, it's still possible to have bugs that can only easily be detected by typed linting. | ||
Asynchronous code with `Promise`s in particular can introduce subtle issues that are completely type-safe. | ||
|
||
Suppose your code is meant to run an asynchronous `readFromCache` function before reading from the file system: | ||
|
||
```ts | ||
import { fs } from 'node:fs/promises'; | ||
import { readFromCache } from './caching'; | ||
|
||
const filePath = './data.json'; | ||
|
||
readFromCache(filePath); | ||
|
||
await fs.rm(filePath); | ||
``` | ||
|
||
Do you see the potential bug? | ||
|
||
If `readFromCache` is asynchronous (returns a `Promise`), then calling it and not awaiting its returned Promise could lead to race conditions in code. | ||
Its asynchronous or delayed logic might not get to reading from the `filePath` before `fs.rm(filePath)` runs. | ||
|
||
This is commonly referred to as a _"floating"_ Promise: one that is created but not appropriately handled. | ||
The [`@typescript-eslint/no-floating-promises`](/rules/no-floating-promises) lint rule would report on that floating Promise: | ||
|
||
```ts | ||
readFromCache(filePath); | ||
// Promises must be awaited, end with a call to .catch, end with a call to .then | ||
// with a rejection handler or be explicitly marked as ignored with the `void` operator. | ||
// eslint(@typescript-eslint/no-floating-promises | ||
``` | ||
|
||
...and can give an editor suggestion to add a missing `await`: | ||
|
||
```diff | ||
- readFromCache(filePath); | ||
+ await readFromCache(filePath); | ||
``` | ||
|
||
Determining whether code is creating a floating Promise is only possible when the types of code are known. | ||
Otherwise, lint rules would have no way of knowing which imports from other files could potentially create a Promise that needs to be handled. | ||
|
||
### Custom Rules | ||
|
||
Typed linting isn't restricted to just typescript-eslint rules. | ||
It can be used in community ESLint plugins, as well as custom rules specific to your project. | ||
|
||
One common example used by teams is to codemod from a deprecated API to its replacement. | ||
Typed linting is often necessary to determine which pieces of code call to the old API. | ||
|
||
As an example, consider the following `fetch()` POST call that sends data to an intake API. | ||
Suppose the intake endpoint is migrating from sending `[string, string]` tuples to sending key-value pairs. | ||
A typed lint rule could determine that the data is in the old format: | ||
|
||
```ts | ||
import { endpoints } from "~/api"; | ||
|
||
const rawData = ["key", "value"] as const; | ||
|
||
await fetch(endpoints.intake, { | ||
data: JSON.stringify(rawData) | ||
// ~~~~~~~ | ||
// Don't pass a tuple to endpoints.intake. Pass a key-value object instead. | ||
// eslint(@my-team/custom-rule) | ||
// ... | ||
method: "POST", | ||
}); | ||
``` | ||
|
||
...and provide a code fix to automatically migrate to the new format: | ||
|
||
```diff | ||
import { endpoints } from "~/api"; | ||
|
||
const rawData = ["key", "value"] as const; | ||
|
||
await fetch(endpoints.intake, { | ||
- data: JSON.stringify(rawData) | ||
+ data: JSON.stringify(Object.fromEntries(rawData)) | ||
// ... | ||
method: "POST", | ||
}); | ||
``` | ||
|
||
Knowing that the `fetch()` call was being sent to `endpoints.intake` and that the type of the data was a tuple takes typed linting. | ||
|
||
That kind of migration codemod is one of the ways typed linting can be utilized for project- or team-specific rules. | ||
See [Developers > Custom Rules](/developers/custom-rules) for more documentation on building your own ESLint rules with typescript-eslint. | ||
|
||
## Enabling Typed Linting | ||
|
||
You can add typed linting to your ESLint configuration by following the steps in [Linting with Type Information](/getting-started/typed-linting). | ||
We recommend doing so by enabling [`parserOptions.projectService`](/packages/parser#projectservice): | ||
|
||
```js title="eslint.config.js" | ||
import tseslint from 'typescript-eslint'; | ||
|
||
export default tseslint.config( | ||
eslint.configs.recommended, | ||
...tseslint.configs.recommendedTypeChecked, | ||
{ | ||
languageOptions: { | ||
parserOptions: { | ||
projectService: true, | ||
tsconfigRootDir: import.meta.dirname, | ||
}, | ||
}, | ||
}, | ||
); | ||
``` | ||
|
||
typescript-eslint will then use TypeScript APIs behind-the-scenes, to, for each file being linted: | ||
|
||
1. Determine the appropriate TSConfig dictating how to generate that file's type information | ||
2. Make APIs available to lint rules to retrieve type information for the file | ||
|
||
Lint rules that opt into type information will then be able to use those APIs when linting your code. | ||
|
||
## Drawbacks of Typed Linting | ||
|
||
Linting with type information comes with two drawbacks: configuration complexity and a performance penalty. | ||
|
||
For configuring typed linting, [`parserOptions.projectService`](/packages/parser#projectservice) solves configuration difficulties for most projects. | ||
The more manual [`parserOptions.project`](/packages/parser#projectservice) is also available for more complex project setups. | ||
See [Troubleshooting & FAQs > Typed Linting](/troubleshooting/typed-linting) for details on common issues. | ||
|
||
For performance, it is inevitable that typed linting will slow your linting down to roughly the speed of type checking your project. | ||
Typed lint rules call to the same TypeScript APIs as the command-line `tsc`. | ||
If linting your project is much slower than running `tsc` on the same set of files, see [Troubleshooting & FAQs > Typed Linting > Performance](/troubleshooting/typed-linting/performance). | ||
|
||
## Final Thoughts | ||
|
||
In our experience, the additional bug catching and features added by typed linting are well worth the costs of configuration and performance. | ||
Typed linting allows lint rules to act with much greater confidence on a wider area of checking, including avoiding unsafe `any` uses, enforcing proper `this` scopes, and catching asynchronous code mishaps. | ||
|
||
If you haven't yet tried out typed linting using the typescript-eslint rules mentioned in this blog post, we'd strongly recommend going through our [Linting with Type Information](/getting-started/typed-linting) guide. |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tangent: It's been bugging me that you can't copy & paste the full code snippet from this page. 😄