Skip to content

Formatters, linters, and compilers: Oh my!

An overview of how those three kinds of static analysis tools work in the JavaScript/TypeScript ecosystem.

Josh Goldberg

Artwork: Susan Haejin Lee

Photo of Josh Goldberg

Josh Goldberg // Open Source Developer

The ReadME Project amplifies the voices of the open source community: the maintainers, developers, and teams whose contributions move the world forward every day.

What is static analysis? Static analysis is a type of tooling that examines your code without actually executing it. This strategy enables static analysis tools to efficiently evaluate your code and provide suggestions for improvements in real time, sometimes as you type in your code editor.


In this Guide, you will learn:

  1. Three common types of static analysis tools and their roles in maintaining code quality, style, and error prevention.

  2. How these tools can be integrated with major code editors and CI systems to provide real-time feedback, automated code improvements, and enforce best practices.

  3. The benefits of using static analysis tools.


Kinds of static analysis

Most static analysis tools fall into one of three categories, in order of complexity (least to most):

  • Formatters: Tools that quickly check and reformat your code for stylistic consistency without changing the runtime behavior of the code.

  • Linters: Tools that detect not just stylistic inconsistency but also potential logical bugs, and often suggest code fixes.

  • Type checkers: Tools that help ensure your code is used in the way it was intended by detecting and warning you about any possible incorrect uses.

What isn’t static analysis

Testing

Unit, end-to-end, and other forms of testing check whether running your code produces the expected results. That disqualifies these tools from being considered “static” analysis. Instead, they’re considered “dynamic” analysis. Common JavaScript unit testing libraries include Jest and Vitest; common JavaScript end-to-end testing libraries include Cypress and Playwright.

Transpilers

Transpilers are tools that transform the syntax of your source code into a different structure while maintaining logical equivalence. Though frequently used alongside static analysis tools, they typically do not perform code analysis themselves. Common JavaScript transpilers, such as Babel and SWC, focus on converting JavaScript code written using newer language features into equivalent code that can run on older environments that have not yet implemented these features.

Transpilers and type checkers are routinely used together, and the combination of the two is often referred to as a compiler.

Formatters

Formatters process your code and produce a version with consistent formatting for elements like semicolons, whitespaces, and other details that don't impact the functionality of the code. They can then warn you of any formatting differences or rewrite the original files with the new, cleaner formatting.

A crucial feature of formatters, which sets them apart from other types of static analysis, is that they do not alter the behavior of your code.They don’t rename, remove, or otherwise mangle constructs declared in code. Formatters are exclusively for formatting.

For example, a formatter might take the following inconsistently formatted code:

1
2
   console.log ("Hello ") ;
 console .log( "world!"  )

…and output the cleaner, more readable equivalent:

1
2
console.log("Hello ");
console.log("world!");

Because formatters enact no logical changes to code, they’re quite fast and are often used by developers to auto-format files on save and/or as a Git commit hook.

The most commonly used JavaScript formatter today is Prettier. It supports most web development languages out of the box, including CSS, HTML, JavaScript, and TypeScript. There are plugins available for Prettier that extend its support for additional programming languages and provide additional code cleanup and sorting capabilities.

Why use a formatter

Formatters are beloved by many developers because they save time in writing code and keep code formatting consistent in projects with multiple developers.

Keeping code formatting clean can be an arduous process without a formatter. Manually typing out spaces or tabs, aligning variables together, and so on can take up a lot of time. Even worse, if you change your formatting preference, going back and updating old code can be even more time-consuming. Formatters apply changes nearly instantaneously, including updating files when a formatting configuration option has changed.

When a software project has multiple contributors, it’s likely that those developers will have different opinions on formatting. When developers have conflicting formatting preferences, it can create ambiguity when editing a file that was authored in another developer's style. Do you align with their style? Use yours only in your edited areas? Rewrite the whole file to yours? By using a dedicated formatter, the project is kept to a consistent style—even if it’s not the exact style preferred by any particular developer.

We want to hear from you! Join us on GitHub Discussions.

Linters

Linters run a set of diagnostic checks on your source code, often called rules. Each rule is typically tailored to one specific check, such as unused variables or preferring a cleaner syntax pattern over an equivalent but messier one. Certain rules have the ability to identify violations and suggest corresponding fixes, such as recommending the use of cleaner syntax as an alternative to more convoluted code.

The most commonly used JavaScript linter today is ESLint. It supports configuring rules on a per-project, per-file, and per-line basis, and comes with several hundred built-in rules. It also allows loading additional rules from community plugins, of which there are many, with purposes ranging from encouraging best practices in popular programming styles such as functional programming to enforcing proper usage of framework APIs such as React.js.

Why use a linter

Linters are powerful, multi-purpose tools that can help developers avoid bugs, adhere to best practices, and learn the correct ways to use larger frameworks and libraries.

Many of the most popular lint rules are tailored to help developers avoid bugs by detecting likely typos in source code. For example, ESLint’s no-unused-expressions rule can detect math operations whose values are ignored, which often occur when a developer accidentally types “+” instead of “+=”:

1
2
3
4
5
6
let value = 0;
if (someCondition) {
  value + 1;
  // The '+' was likely supposed to be '+='
}

Because linters—as with all static analysis—run quickly on code before it’s run, they can catch these kinds of bugs automatically and before code is sent out for review.

Linters can not only identify errors but also automatically correct instances of API misuse, thereby serving as a valuable educational tool for developers to improve their use of APIs. For example, the eslint-plugin-react-hooks plugin for React projects will let developers know if they’re misusing React’s Hooks feature, and will direct them to the relevant React documentation for hooks:

1
2
3
4
5
6
7
function MyReactComponent({ value }) {
  if (value) {
    useEffect(() => console.log("Received:", value));
		// React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render.
		// <https://reactjs.org/docs/hooks-rules.html>
  }
}

Don’t use linters for formatting!

One of the most common mistakes developers make when setting up their environment is using a linter, such as ESLint, to mandate code formatting. Although lint rules can enforce some formatting preferences in code, they’re slower, less consistent, and more prone to missing edge cases when compared to dedicated formatters.

The ESLint project generally recommends using a linter only to highlight errors or objectively better ways of writing code.

Type checkers

Type checkers are tools based on the concept of "types," which define permissible values in code. By analyzing a project's source code, type checkers discern the intended use of code constructs, then issue warnings if potential mismatches between intent and actual execution are detected.

Today, the most commonly used type checker for JavaScript code is provided by TypeScript. TypeScript is a superset of JavaScript that introduces additional syntax designed to provide direct guidance to the type checker regarding the intended functionality of the code. By analyzing JavaScript and TypeScript files, the TypeScript type checker can identify and report any type errors present in the code.

For example, the following code snippet uses type annotations to indicate what types the logSum function’s parameters are meant to be, so the TypeScript type checker could detect that the logSum(123, "four") call is providing a string instead of a number:

1
2
3
4
5
6
function logSum(first: number, second: number) {
	console.log("Sum:", first + second);
}
logSum(123, "four");
// Argument of type 'string' is not assignable to parameter of type 'number'.

Why use a type checker

TypeScript brings with it three main benefits: 

  1. A precise documentation format

  2. Augmented developer tooling

  3. Bug catching

Presently, JavaScript language does not have a standard way to document the intent of code, such as what types the previous function’s parameters should be. Community standards such as JSDoc exist to use comments for documentation purposes, but they aren’t formalized, so different projects can use them in drastically different ways. TypeScript’s syntax, on the other hand, is validated by the type checker, which can also detect areas that are missing type annotations to indicate otherwise mysterious areas of code.

Another benefit of type systems is that they can augment text editor tooling as you code. Editors that have integrated TypeScript support, such as VS Code, can provide a suite of navigation helpers and on-demand refactors. These include tools to find all references to an item, jump to where an item is declared, or rename an item along with all references to it. These language services can help speed up laborious chores during development—and prevent small human errors from creeping in.

Among the most frequent bugs that occur in JavaScript applications are those resulting from inconsistencies between the intended use of language constructs and their actual usage. As you write and rewrite code, it can be difficult to keep track of all the places where each function, variable, and other construct are used. Suppose a frequently used function is modified to accept an object with two properties instead of two separate parameters. In such a scenario, it might be difficult to find all the places that refer to that function, especially if the function is stored in variables and as properties of other objects. TypeScript’s type system, however, could detect improper uses of the function and indicate which areas of code haven’t been updated for the new function type.

Type checked lint rules

TypeScript’s static analysis APIs can be used to augment the capabilities of other static analysis tools. One common use is for lint rules that can act on types of code as well as the code’s raw syntax. Without TypeScript, ESLint’s lint rules can only act on raw code syntax without a type system level understanding of the code’s intent. But with TypeScript, lint rules can use type system information to provide deeper insights into code.

The tooling that allows using TypeScript with ESLint is typescript-eslint. It’s necessary for ESLint to understand code written with TypeScript’s syntax extensions to JavaScript. It also provides lint rules that tap into TypeScript’s type system. For example, typescript-eslint’s await-thenable rule can detect an await of a value that’s not a Thenable such as a Promise:

1
2
3
4
5
6
7
8
async function logBeforeAndAfter(action: () => void) {
  console.log("Before...");
  // Unexpected `await` of a non-Promise (non-"Thenable") value.
  await action();
  
  console.log("After.");
}

Integrating with static analysis tooling

Each piece of static analysis described in this article can be tightly integrated into most software development workflows. ESLint, Prettier, and TypeScript offer integrations with all major editors, including Emacs, Vim, and VS Code, enabling developers to run these tools in real time as they type their code. With this integration, any detected issues are reported immediately, and the tooling displays them visually to the developer

Each tool can also be run separately on the command line to validate that the project doesn’t break any of the tool’s rules. It’s common for developers to use CI systems such as GitHub Actions to run these tools on each commit. Blocking merging pull requests on tooling error reports ensures developers fix issues detected by the tools.

Tip: It can be useful to have a job defined for each tool so that they run separately from each other. This approach allows for precise identification of which tools failed whenever a commit introduces violations.

You can see these tools in action together in template-typescript-node-package: a template TypeScript repository with comprehensive formatting, linting, type checking, and other tooling built-in.

Takeaways

The use of static analysis tools like formatters, linters, and type checkers can greatly improve code quality and the development process. Incorporating popular tools such as Prettier, ESLint, and TypeScript allows developers to maintain consistent formatting, follow best practices, and minimize potential errors in their code. Consider exploring and integrating these powerful tools into your code editor and CI systems for a smoother, more efficient, and robust software development experience.

Josh Goldberg is an independent, full-time open source developer. He works on projects in the TypeScript ecosystem, most notably typescript-eslint, the tooling that enables ESLint and Prettier to run on TypeScript code. Josh is also the author of the O’Reilly Learning TypeScript book, a Microsoft MVP for developer technologies, and an active conference speaker. His personal projects range from static analysis to meta-languages to recreating retro games in the browser. Also cats.

About The
ReadME Project

Coding is usually seen as a solitary activity, but it’s actually the world’s largest community effort led by open source maintainers, contributors, and teams. These unsung heroes put in long hours to build software, fix issues, field questions, and manage communities.

The ReadME Project is part of GitHub’s ongoing effort to amplify the voices of the developer community. It’s an evolving space to engage with the community and explore the stories, challenges, technology, and culture that surround the world of open source.

Follow us:

Nominate a developer

Nominate inspiring developers and projects you think we should feature in The ReadME Project.

Support the community

Recognize developers working behind the scenes and help open source projects get the resources they need.

Thank you! for subscribing