diff --git a/src/_data/all_authors.json b/src/_data/all_authors.json index 7b37ae1cb..6f25a3f3e 100644 --- a/src/_data/all_authors.json +++ b/src/_data/all_authors.json @@ -200,5 +200,17 @@ "twitter_username": "antfu7", "github_username": "antfu", "location": "undefined" + }, + "joshuakgoldberg": { + "username": "joshuakgoldberg", + "name": "Josh Goldberg", + "title": "Committer Team Member", + "website": "https://joshuakgoldberg.com", + "avatar_url": "https://avatars.githubusercontent.com/u/3335181?v=4", + "bio": "An open source maintainer in the TypeScript ecosystem.", + "github_username": "JoshuaKGoldberg", + "mastodon_url": "https://fosstodon.org/@JoshuaKGoldberg", + "twitter_username": "JoshuaKGoldberg2", + "location": "undefined" } -} \ No newline at end of file +} diff --git a/src/_data/team.json b/src/_data/team.json index c1e0ce111..fae94600f 100644 --- a/src/_data/team.json +++ b/src/_data/team.json @@ -458,4 +458,4 @@ "location": "Canada" } ] -} \ No newline at end of file +} diff --git a/src/_includes/partials/author_bios/joshuakgoldberg.md b/src/_includes/partials/author_bios/joshuakgoldberg.md new file mode 100644 index 000000000..1980e0cfc --- /dev/null +++ b/src/_includes/partials/author_bios/joshuakgoldberg.md @@ -0,0 +1 @@ +I'm involved with projects in the TypeScript ecosystem such as typescript-eslint, and wrote *Learning TypeScript* (O'Reilly). diff --git a/src/content/blog/2024-12-04-differences-between-linting-and-type-checking.md b/src/content/blog/2024-12-04-differences-between-linting-and-type-checking.md new file mode 100644 index 000000000..85e910fd0 --- /dev/null +++ b/src/content/blog/2024-12-04-differences-between-linting-and-type-checking.md @@ -0,0 +1,274 @@ +--- +layout: post +title: "Differences between linting and type checking" +teaser: "Linters such as ESLint and type checkers such as TypeScript catch different areas of code defects — and are best used in conjunction with each other." +authors: + - joshuakgoldberg +categories: + - Storytime +tags: + - Linting +--- + +If you're a JavaScript developer today, there's a good chance you're using ESLint and/or TypeScript to assist development. +Those two tools are common examples of their kind of tooling: ESLint is a common *linter*, whereas TypeScript is a common *type checker*. + +Linters and type checkers are two kinds of tools that both analyze code and report on detected issues. +They may seem similar at first. +They both fall under the category of *static analysis*. +However, the two kinds of tools detect very different issues and are useful in different ways. + +## What is static analysis? + +Static analysis is the inspection of source code without executing it. +This differs from *dynamic analysis*, in which source code is inspected while it is executed. +As such, dynamic analysis brings with it the inherent danger of executing malicious code or creating side effects while static analysis is safe to execute regardless of the source code. + +Static analysis can be immensely helpful for improving code readability, reliability, and overall quality. +Many developers rely on static analysis to enforce consistent code formatting and style, to ensure code is well-documented, and to catch likely bugs. +Because static analysis runs on source code, it can suggest improvements in editors as code is written. + +We'll focus in this blog post on ESLint and TypeScript: + +* **ESLint**: executes individually configurable checks known as "lint rules" +* **TypeScript**: collects all files into a full understanding of the project + +ESLint and TypeScript use some of the same forms of analysis to detect defects in code. +They both analyze how scopes and variables are created and used in code, and can catch issues such as referencing a variable that doesn't exist. +We'll explore the different ways the two use information from analyzing your code. + +## Digging deeper into linting vs. type checking + +Type checkers make sure the *intent* behind values in code matches the *usage* of those values. +They check that code is "type-safe": values are used according to how their types are described as being allowed. + +For example, TypeScript would report a type error on the following `logUppercase(9001)` call, because `logUppercase` is declared as receiving a `string` rather than a `number`: + +```ts +function logUppercase(text: string) { + console.log(text.toUpperCase()); +} + +logUppercase(9001); +// ~~~~ +// Argument of type 'number' is not assignable to parameter of type 'string'. +``` + +Compiled languages such as Java and Rust perform type checking as part of their compilation step. +Because JavaScript is an interpreted language, TypeScript is run separately. + +TypeScript generally does not attempt to look for defects that are only likely to occur. +It generally only looks for uses of code that are certainly wrong. +Nor does the type checking in TypeScript enforce subjective opinions that might change between projects. + +Linters, on the other hand, primarily target likely defects and can also be used to enforce subjective opinions. +ESLint and other linters catch issues that may or may not be type-safe but are potential sources of bugs. +Many developers rely on linters to make sure their code follows framework and language best practices. + +For example, developers sometimes leave out the `break` or `return` at the end of a `switch` statement's `case`. +Doing so is type-safe and permitted by JavaScript and TypeScript. +In practice, this is almost always a mistake that allows the next `case` statement to run accidentally. +ESLint's [`no-fallthrough`](https://eslint.org/docs/latest/rules/no-fallthrough) can catch that likely mistake: + +```ts +function logFruit(value: "apple" | "banana" | "cherry") { + switch (value) { + case "apple": + console.log("🍏"); + break; + + case "banana": + console.log("🍌"); + + // eslint(no-fallthrough): + // Expected a 'break' statement before 'case'. + + case "cherry": + console.log("🍒"); + break; + } +} + +// Logs: +// 🍌 +// 🍒 +logFruit("banana"); +``` + +Another way of looking at the differences between ESLint and TypeScript is that TypeScript enforces what you *can* do, whereas ESLint enforces what you *should* do. + +### Granular Extensibility + +Another difference between ESLint and TypeScript is how flexibly their areas of responsibility can be configured. + +TypeScript is configured by a set list of compiler options on a project level. +Its [`tsconfig.json` ("TSConfig") files](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) allow for compiler options that change type checking for all files in the project. +Those compiler options are set by TypeScript and generally change large swathes of type checking behavior. + +ESLint, on the other hand, runs with a configurable set of lint rules. +Each lint rule can be granularly configured. +If you don't like a particular lint rule, you can always turn it off for a line, set of files, or your entire project. + +ESLint can also be augmented by **plugins** that add new lint rules. +Plugin-specific lint rules extend the breadth of code checks that ESLint configurations can pick and choose from. + +For example, this ESLint configuration enables the recommended rules from [`eslint-plugin-jsx-a11y`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y), a plugin that adds checks for accessibility in projects using JSX libraries such as Solid.js and React: + +```js title="eslint.config.js" +import js from "@eslint/js"; +import jsxA11y from "eslint-plugin-jsx-a11y" + +export default [ + js.configs.recommended, + jsxA11y.flatConfigs.recommended, + // ... +]; +``` + +A project using the JSX accessibility rules would then be told if their code violates common accessibility guidelines. +For example, rendering a native `` tag without descriptive text would receive a report from [`jsx-a11y/alt-text`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/alt-text.md): + +```tsx +const MyComponent = () => ; +// ~~~~~~~~~~~~~~~~~~~~~~~~~ +// eslint(jsx-a11y/alt-text): +// img elements must have an alt prop, either with meaningful text, or an empty string for decorative images. +``` + +By adding in rules from plugins, ESLint configurations can be tailored to the specific best practices and common issues to the frameworks a project is built with. + +### Areas of Overlap + +ESLint and TypeScript operate differently and specialize in different areas of code defects. +Some code defects straddle the line between "best practices" and "type safety", and so can be caught by both linting and type checking. +Those defects can often be caught by both ESLint and TypeScript. + +To make the most of both tools, we recommend: + +* In your ESLint configuration file, use the [ESLint `js.configs.recommended` config](https://eslint.org/docs/latest/use/getting-started#configuration), at least the [`tseslint.configs.recommended` config from typescript-eslint](https://typescript-eslint.io/getting-started/typed-linting), and any community plugins relevant to your project's libraries and frameworks +* In your TypeScript configuration file, enable [`strict` mode](https://www.typescriptlang.org/tsconfig/#strict) to catch as many type safety issues as possible + +typescript-eslint's recommended preset configs disable core ESLint rules that are not helpful with TypeScript. +The configs leave on any core ESLint rules that are useful alongside type checking. + +#### Unused Locals and Parameters + +The only TypeScript compiler options we recommend keeping off when using linting are those that enable checking for unused variables: + +* [`noUnusedLocals`](https://www.typescriptlang.org/tsconfig/#noUnusedLocals): which reports on unused declared local variables +* [`noUnusedParameters`](https://www.typescriptlang.org/tsconfig/#noUnusedParameters): which reports on unused function parameters + +Those compiler options are useful when not using a linter. +However, they aren't configurable the way lint rules are, and so can't be configured to higher or lower strictness levels per a project's preferences. +The compiler options are hardcoded to always ignore any variable whose name begins with `_`. + +As an example, the following `registerCallback` function declares two parameters for its callbacks -`id` and `message`- but the developer using it only needed `message`. +TypeScript's `noUnusedParameters` compiler option would not flag the unused parameter `_`: + +```ts +type Callback = (id: string, message: string) => void; + +declare function registerCallback(callback: Callback): void; + +// We only want to log message, not id +registerCallback((_, message) => console.log(message)); +``` + +Unused variables in JavaScript can also be caught by ESLint's [`no-unused-vars`](https://eslint.org/docs/latest/rules/no-unused-vars) rule; when in TypeScript code, the [`@typescript-eslint/no-unused-vars`](https://typescript-eslint.io/rules/no-unused-vars) is preferable instead. +The lint rules by default also ignore variables whose name begins with `_`. +They additionally ignore parameters that come before any parameter that is itself used. + +Some projects prefer to never allow unused parameters regardless of name or position. +These stricter preferences help prevent API designs that lead developers to create many unused parameters. + +A more strict ESLint configuration would be able to report on the `_` parameter: + +```ts +/* eslint @typescript-eslint/no-unused-vars: ["error", { "args": "all", "argsIgnorePattern": "" }] */ + +type Callback = (id: string, message: string) => void; + +declare function registerCallback(callback: Callback): void; + +// We only want to log message, not id +registerCallback((_, message) => console.log(message)); +// ~ +// eslint(@typescript-eslint/no-unused-vars): +// '_' is declared but never used. +``` + +That extra level of configurability provided by the `no-unused-vars` rules can allow them to act as more granularly configurable versions of their equivalent TypeScript compiler options. + +> 💡 See [`no-unused-binary-expressions`: From code review nit to ecosystem improvements](/blog/2024/10/code-review-nit-to-ecosystem-improvements) for more areas of code checking with partial overlap between linting and type checking. + +## Is a ESLint Still Useful With TypeScript? + +**Yes.** + +If you are using TypeScript, it is still very useful to use ESLint. +In fact, ESLint and TypeScript are at their most powerful when used in conjunction with each other. + +### ESLint, With Type Information + +Traditional lint rules run on a single file at a time and have no knowledge of other files in the project. +They can't make decisions on files based on the contents of other files. + +However, if your project is set up using TypeScript, you can opt into "type checked" lint rules: rules that can pull in type information. +In doing so, type checked lint rules can make decisions based on other files. + +For example, [`@typescript-eslint/no-for-in-array`](https://typescript-eslint.io/rules/no-for-in-array) is able to detect `for...in` loops over values that are array types, even if the values come from other files. +TypeScript would not report a type error for a `for...in` loop over an array because doing so is technically type-safe and might be what a developer intended. +A linter, however, could be configured to notice that the developer probably made a mistake and meant to use a `for...of` loop instead: + +```ts +// declare function getArrayOfNames(): string[]; +import { getArrayOfNames } from "./my-names"; + +for (const name in getArrayOfNames()) { + // eslint(@typescript-eslint/no-for-in-array): + // For-in loops over arrays skips holes, returns indices as strings, + // and may visit the prototype chain or other enumerable properties. + // Use a more robust iteration method such as for-of or array.forEach instead. + console.log(name); +} +``` + +Augmenting ESLint with information from TypeScript makes for a more powerful set of lint rules. +See [Typed Linting: The Most Powerful TypeScript Linting Ever](https://typescript-eslint.io/blog/typed-linting) for more details on typed linting with typescript-eslint. + +### TypeScript, With Linting + +TypeScript adds extra complexity to JavaScript. +That complexity is often worth it, but any added complexity brings with it the potential for misuse. +Linters are useful for stopping developers from making TypeScript-specific blunders in code. + +For example, TypeScript's `{}` ("empty object") type is often misused by developers new to TypeScript. +It visually looks like it should mean any `object`, but actually means any non-`null`, non-`undefined` value --- including primitives such as `number`s and `string`s. +[`@typescript-eslint/no-empty-object-type`](https://typescript-eslint.io/rules/no-empty-object-type) catches uses of the `{}` type that likely meant `object` or `unknown` instead: + +```ts +export function logObjectEntries(value: {}) { + // ~~ + // eslint(@typescript-eslint/no-empty-object-type): + // The `{}` ("empty object") type allows any non-nullish value, including literals like `0` and `""`. + // - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. + // - If you want a type meaning "any object", you probably want `object` instead. + // - If you want a type meaning "any value", you probably want `unknown` instead. + console.log(Object.entries(value)); +} + +logObjectEntries(0); // No type error! +``` + +Enforcing language-specific best practices with ESLint helps developers learn about and correctly use TypeScript. + +## Conclusion + +Linters such as ESLint and type checkers such as TypeScript are both valuable assets for developers. +The two catch different areas of code defects and come with different philosophies around configurability and extensibility. + +* TypeScripts checks that code is "type-safe": enforcing what you *can* write +* ESLint checks that code adheres to best practices and is consistent: enforcing what you *should* write + +Put together, the two tools help projects write code with fewer bugs and more consistency. +We recommend that any project that uses TypeScript additionally uses ESLint.