I’ve been doing a lot of advocacy work on how to properly use formatters such as Prettier alongside linters such as ESLint.
- Formatters should be used for formatting (changes to code trivia/whitespace that don’t impact runtime, such as tabs vs. spaces)
- Linters should be used for logical issues (runtime behavior) and style (non-logical issues that do impact runtime behavior, such as sorting imports)
The delineation of formatting vs. stylistic concerns is normally pretty straightforward to follow. Turning off any ESLint rule that would conflict with Prettier is not much work these days: neither ESLint nor typescript-eslint’s core rulesets enable any rules that violate that philosophy, and eslint-config-prettier can do it for you if you’re working with other configs.
But! There are some edge cases in formatting/style that can seem to cross the divide and make it unclear when to use which tool. I’ll cover some of those surprising intricacies in this post, along with how I’d suggest resolving them.
Formatting Can Technically Change Behavior
Did you know that JavaScript’s Function.prototype.toString()
returns the string equivalent of a function?
That means any tool that modifies the text of a function -including formatters, minifiers, and transpilers- technically can change the runtime behavior of code.
For example, this code would log a different message based on whether it’s formatted with spaces or tabs:
function greet(name) {
console.log(`Hello, ${name}!`);
}
console.log(greet.toString().includes("\t") ? "tab" : "no tab");
In practice, exceedingly little real code stringifies functions, let alone cares about the formatting of the result. I’ve never seen it be an issue. But it is a nifty proof of concept that these concerns can matter!
AST Modifications
Formatters such as Prettier generally don’t make modifications to code that change how the code is represented by tooling (generally known as its AST, or Abstract Syntax Tree). That means some changes can surprisingly be considered out-of-scope for a formatter.
Most notably (for me), Prettier does not have an option to enforce curly brackets.
Doing so would change the consequent (what follows the if(...)
) of if
statements from a Statement (e.g. console.log()
) to a Block (e.g. { console.log(); }
).
That limitation makes sense on its own, but presents an inconvenience: enforcing curly brackets is arguably a formatting concern, not a stylistic one - and so shouldn’t be handled by linters! 😫.
For a while, I compromised my principles and would include usage of the curly
rule in my ESLint configs.
But that always felt wrong - even if the formatting couldn’t reasonably handle this AST modification on its own, enforcing curly brackets really is something that should be handled by the formatter.
prettier-plugin-curly
That’s why I recently wrote & published my first Prettier plugin: prettier-plugin-curly
.
It adds the enforcement of consistent brace style (i.e. the curly
rule’s all
option) to Prettier.
I now use it in my template-typescript-node-package’s .prettierrc
.
{
"plugins": ["prettier-plugin-curly"],
"useTabs": true
}
If you’re interested in enforcing curly brackets as part of your formatter, I’d encourage you to try prettier-plugin-curly
out.
It’s the first time I’ve written a Prettier plugin and I’d love to be told how to improve it. ❤️
Order Matters
One last sometimes-surprisingly-impactful concern is ordering. Many developers -myself included- prefer ordering file imports, properties on objects and types, and other constructs in a predictable way, to make it easier to scan through them. It’s tempting to suggest ordering as within the realm of a formatter.
Imports
However, order does impact runtime behavior - especially for imports!
The order your files import
and/or require
each other can make a difference because code files sometimes trigger side effects when they’re run.
Consider this set of three files, where running index.js
causes logs to run in the imported files in order of import:
// index.js
import { b } from "./b.js";
import { a } from "./a.js";
// a.js
console.log("A!");
export const a = "a";
// b.js
console.log("B!");
export const b = "b";
Your code might be triggering more intensive side effects, such as registering CSS styles, calling fetch()
, or reading/writing files on disk.
Changing the order of module import
s is generally too risky for formatters.
Property Destructures
In fact, even the ordering of destructured object properties can make a difference! Object properties defined as getters can introduce side effects.
For example, this code block logs a: 0
and b: 1
now, but alphabetizing the { b, a }
to { a, b }
would cause it to instead log a: 1
and b: 0
:
let count = 0;
const values = {
get a() {
return `a: ${count++}`;
},
get b() {
return `b: ${count++}`;
},
};
const { b, a } = values;
console.log(a);
console.log(b);
Changes in runtime behavior from sorting imports or property destructures are still pretty rare, though they can happen (especially in the case of imports). I don’t recommend using a Prettier plugin for sorting.
eslint-plugin-perfectionist
Instead, I recommend using eslint-plugin-perfectionist
to apply sorting at the lint level.
This plugin provides a nice comprehensive set of rules for sorting constructs in JavaScript and TypeScript code.
{
extends: ["plugin:perfectionist/recommended-natural"],
plugins: ["perfectionist"],
}
eslint-plugin-perfectionist
is also pretty new -released mid-2023- and I’d highly recommend trying it out.
It’s enabled in my template-typescript-node-package’s .eslintrc.cjs
.
Summarizing
I think a succinct set of guidelines out of all of this would be:
- Formatters should never change code behavior (except for stringifying functions)
- Linting for style should auto-fix any remaining changes that don’t impact code behavior (except for rare edge cases such as order-dependent side effects)
What do you think? Are there other edge cases that need to be handled? Please let me know!
Resources
You can read more about the concerns of formatters vs. linters in the following posts I’ve written:
- GitHub ReadME Guide: Formatters, linters, and compilers: Oh my!
- My Configuring ESLint, Prettier, and TypeScript Together guide
- typescript-eslint’s What About Formatting? page
This blog post was inspired by a timely discussion thread on Twitter with @azat_io_en and @helloklose.
Thanks to Azat for sharing out eslint-plugin-perfectionist
and Oksel for asking great questions! 🙌