Content-Length: 13267 | pFad | http://github.com/typescript-eslint/typescript-eslint/pull/10025.diff
thub.com diff --git a/.cspell.json b/.cspell.json index 93e100a70adf..deb654ab3735 100644 --- a/.cspell.json +++ b/.cspell.json @@ -83,6 +83,7 @@ "Cheung", "codebases", "Codecov", + "codemod", "contravariant", "Crockford", "declarators", diff --git a/docs/getting-started/Typed_Linting.mdx b/docs/getting-started/Typed_Linting.mdx index 2c76b9dfff73..b1e6a9d16663 100644 --- a/docs/getting-started/Typed_Linting.mdx +++ b/docs/getting-started/Typed_Linting.mdx @@ -16,6 +16,9 @@ To tap into TypeScript's additional powers, there are two small changes you need 2. Add `languageOptions.parserOptions` to tell our parser how to find the TSConfig for each source file. ```js title="eslint.config.mjs" +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + export default tseslint.config( eslint.configs.recommended, // Remove this line diff --git a/packages/website/blog/2024-09-30-typed-linting.md b/packages/website/blog/2024-09-30-typed-linting.md new file mode 100644 index 000000000000..4037d6053ecb --- /dev/null +++ b/packages/website/blog/2024-09-30-typed-linting.md @@ -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. + + + +## 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 + + + +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.Fetched URL: http://github.com/typescript-eslint/typescript-eslint/pull/10025.diff
Alternative Proxies: