No software team should spend time and mental resources arguing about code formatting. The same goes for code practices that some of the team members might consider unsafe or counter-productive. To ensure that, configure your project to use the power duo of Prettier and ESLint — for code formatting and code linting.

A Real-Life Scenario

Imagine reviewing a PR, and immediately noticing a formatting issue: a dangling comma is missing. If you don’t care about being called a nitpicker, you highlight this as a change request.

Down a few more lines of code, the same issue repeated. Scrolling through the PR, you see several more instances of it. Do you add a comment for each? Do you simply put “and so on?” Without even getting to review the business logic, you have already spent 5 minutes and stumbled upon several decisions to be made that could have easily been avoided.

Having formatting errors automatically corrected helps never to run into this kind of situation.

Linter does something similar, but for lousy code practices. Instead of automatically correcting an issue, most of the time, it only highlights that a particular piece of code goes violates the rules agreed upon by the project team. This helps the author to catch and fix the issue before the code review starts.

If you get this meme you probably don’t need to read this article.
If you get this meme you probably don’t need to read this article.

What Is Code Formatting and Why Does It Matter?

Code formatting is rather about aesthetics than logic. Things like using single quotes instead of double (if the language allows that) has only residual, if any, effect on performance, complexity or readability.

Having a uniform code formatting however has a huge impact on readability.

When I suddenly see a double-quoted string instead of a single-quoted one, a bunch of questions automatically pop into my head: why is it double-quoted while the rest of the codebase is single? Is it on purpose? If not, is it a mistake? Should I fix this? Should I highlight this? Who added this piece of code (if the quotes are in a refactored code)? When did they do this? Maybe it’s a legacy code?

This slows me down every time I scan through the code. And some people simply take “bad” (= not the one they prefer or the one that was agreed upon) formatting as a personal insult, which makes a dent in their motivation to work on the project.

What Is Linting and Why Does It Matter?

Simply put, linting means avoiding code that might be unsafe, has poor readability, or is potentially hard to maintain.

Do not re-assign function parameters. Do not make var what is supposed to be const. Make sure an async function call is always prepended with an await. Always use a dangling comma.

99% of the time, ignoring those guidelines introduces room for logical errors and bugs — especially when the code gets refactored.

Why Formatting and Linting Must Be Done Automatically?

If something must be done 100% of the time, the best way to ensure that is to automate it. Otherwise whoever is responsible, will sooner or later slip and forget.

If there are several people working on the code, the chance one of them forgets to apply formatting and linting rules before every push multiplies.

Modern setups allow different approaches to automating formatting and blocking non-linted code from reaching the repository.

Configuring Linting and Code Formatting for TypeScript

The following tools are specific for Node.js / TypeScript development done using the VSCode editor. However, there are similar tools available for most modern programming languages and other editors.

ESLint

ESLint is the de-facto standard tool for linting and sometimes formatting. Its primary goal is to find and print linting and formatting errors. Integrated with an editor such as VSCode via a plugin, it can highlight errors directly in the IDE. Some rules can be applied automatically. In other words, ESLint can change the code to conform with the rules. Others require refactoring from the developer.

ESLint Rules and Configs

There are more than a hundred rules that ESLint supports. Each can be enabled or disabled via ESLint config. Often there’s a more complex configuration available through specific rule options, e.g., treat something as an error with certain exceptions.

There are several popular combinations of ESLint rules distributed as NPM packages: AirBnB, Recommended, Standard.

config-stats.png
config-stats.png

You probably want to pick the ruleset that has the most rules you agree with. All rules from those sets can be overridden in the project ESLint config.

The Golden Rule

While ESLint can highlight and correct both formatting and non-formatting (like “don’t re-assign function parameters”) rules, I want those types of rules to be treated differently.

Formatting rules must be always applied automatically with no exceptions.

On the other hand, non-formatting rules must never be applied automatically, and I want to be able to commit them locally (but not push to the remote).

Prettier

To split the handling of these two types of rules, and to speed up applying formatting rules, another tool can be used - Prettier. It’s a command line tool with the sole purpose of formatting files according to its own opinionated rule set.

There is some room for configuration (yes, you’ll be able to pick a single or a double quotes) and a VSCode plugin.

If you use Prettier, you will have to install an extra package for ESLint that disables rules that are conflicting with what Prettier does. And make sure to not enable those rules in the ESLint config (otherwise after Prettier applies formatting will always make ESLint yield errors).

Automating In Editor

Since applying formatting will be automated, it should be done as early as possible after the code is added. VSCode allows the code formatting to be applied each time a file is saved. See the “Step-by-Step” section below for VSCode-specific configuration.

Automating with a git Commit/Push Hook

Since a rogue developer might not enable the format-on-save option in their code editor (or their editor might not support it), it makes sense to add a Prettier hook that will automatically apply formatting on each commit.

To make sure that no code with unfixed linter errors gets into the repository, add a lint hook on push to the remote.

Step-by-Step TypeScript Project Configuration

For the tutorial below, I will create a dummy TypeScript API using Apollo GraphQL server.

Initial Project Configuration

  1. Initiate the project
mkdir prettylint && cd prettylint && npm init -y
  1. Install your “business” packages
npm i @apollo/server graphql typescript
  1. Add the package.json prop to enable import (we’re going to use ESModules in this example)
"type": "module",
  1. Create src/index.ts file with some formatting/lint errors:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

import { formatComplex, formatNameShort } from './utils/format.js';

const books = [
  {
    title: 'The Awakening',
    author: 'Kate Chopin',
  },
  {
    title: 'City of Glass',
    author: 'Paul Auster',
  },
];

const persons = [
  {
    firstName: 'John',
    lastName: 'Appleseed',
  },
  {
    firstName: 'Robert',
    lastName: 'Paulson',
  },
];

const typeDefs = `#graphql
  type Book {
    title: String
    author: String
  }

  type Person {
    firstName: String
    lastName: String
    shortName: String
  }

  type Query {
    books: [Book!]!
    persons: [Person!]!
  }
`;

const resolvers = {
  Query: {
    books: () => {
      console.log('this will return books');
      return books
    },
    persons: () => {
      return persons.map((person) => ({
        ...person,
        shortName: formatNameShort(person.firstName, person.lastName),
        complexName: formatComplex(person.firstName, person['lastName'], function () {
          console.log('donk');
        }),
      }));
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`🚀  Server ready at: ${url}`);
  1. Add another src/utils/format.ts file:
export  const formatNameShort = (first, last) =>
	`${first.substring( 0,1) }. ${last}`;

export const formatComplex = (first: string,
	last: string,onComplete: () => void,
): string => {
	last = String(5);
	   first = first + "changed";
	var k = 0;
	var m = ["a", "b"];
	var n = ["a", "b"];
	first = String(k > 0 ? "ye" : k--);
	return first;
};
  1. Create and add the following to tsconfig.json:
{
  "compilerOptions": {
    "rootDirs": ["src"],
    "outDir": "dist",
    "lib": ["es2020"],
    "target": "es2020",
    "module": "esnext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "types": ["node"],

    "paths": {
      "*": ["./src/*"]
    },

    "baseUrl": ".",

    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}
  1. Update the scripts section in the package.json:
  "scripts": {
    "compile": "tsc",
    "start": "npm run compile && node ./dist/index.js"
  },
  1. Run it:
npm start
  1. Open http://localhost:4000 and try the “books” query.

apollo.png
apollo.png

Add Prettier

  1. If you haven’t done it already, install the Prettier plugin for your VSCode (or other editor).

vscode-prettier.png
vscode-prettier.png

  1. Add a .prettierrc file
{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100,
  "jsxSingleQuote": true
}
  1. Cmd+Shift+P → “Workspace Settings” → Enter; “Format on Save”; Check the Editor: Format on Save checkbox.
  2. Next, search for the “Formatter” option in the Workspace Settings, and set it to “Prettier”

vscode-formatter.png
vscode-formatter.png

  1. Get back to index.ts and hit Cmd+S. The file will be auto-formatted. If that doesn’t happen, make sure that:
    1. Both “Format on Save” and “Editor: Default Formatter” options are set on the Workspace level, and not set on any other level.
    2. Prettier plugin is installed and enabled (sometimes the editor must be restarted)
    3. There isn’t an .editorconfig file with potentially conflicting configuration
  2. Do the same for utils/format.ts

Add ESLint

  1. Install the necessary packages
npm i --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-import
  1. Add the ESLint config file .eslintrc.cjs
module.exports = {
  env: {
    node: true,
  },
  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
  plugins: ['import', 'prettier'],
  root: true,
  rules: {
    'import/no-extraneous-dependencies': 'off',
    'no-console': ['error', { allow: ['warn', 'error'] }],
    'no-param-reassign': ['error', { props: false }],

    'no-unused-vars': 'off',
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/explicit-function-return-type': [
      'error',
      {
        allowExpressions: true,
        allowHigherOrderFunctions: true,
        allowTypedFunctionExpressions: true,
      },
    ],
  },
  ignorePatterns: ['.eslintrc.cjs', 'dist/*'], // this is necessary for thing to work
};
  1. Test manually running the ESLint
npx eslint .

You should see the output full of issues similar to

/Users/sp/projects/prettylint/src/index.ts
  50:7   error  Unexpected console statement  no-console
  58:11  error  Unexpected console statement  no-console
  74:1   error  Unexpected console statement  no-console

/Users/sp/projects/prettylint/src/utils/format.ts
  1:32  error  Missing return type on function            @typescript-eslint/explicit-function-return-type
  3:60  error  'onComplete' is defined but never used     @typescript-eslint/no-unused-vars
  4:3   error  Assignment to function parameter 'last'    no-param-reassign
  4:3   error  'last' is assigned a value but never used  @typescript-eslint/no-unused-vars
  5:3   error  Assignment to function parameter 'first'   no-param-reassign
  6:3   error  Unexpected var, use let or const instead   no-var
  7:3   error  Unexpected var, use let or const instead   no-var
  7:7   error  'm' is assigned a value but never used     @typescript-eslint/no-unused-vars
  8:3   error  Unexpected var, use let or const instead   no-var
  8:7   error  'n' is assigned a value but never used     @typescript-eslint/no-unused-vars
  9:3   error  Assignment to function parameter 'first'   no-param-reassign

 14 problems (14 errors, 0 warnings)
  3 errors and 0 warnings potentially fixable with the `--fix` option.
  1. Now it’s time to have those highlighted by the editor. Install and enable the ESLint extension.

vscode-eslint.png
vscode-eslint.png

  1. Restart the IDE and observe

lint-errors.png
lint-errors.png

Add Pre-Commit and Pre-Push Hooks

To recap, I want to be able to commit only formatted code allowing linting errors. I don’t however want any code with linting errors in the repository.

Thus, I configure automatic code formatting in the pre-commit hook, and the linting of only staged files with the pre-push hook. That can be easily done with husky, and lint-staged packages.

Bonus: Rome — An Alternative to the ESLint + Prettier Duo

The Rome package is designed to replace the combination of Prettier and ESLint. Just like Prettier, it is an opinionated formatter with some available configuration, and has its own set of linting rules, unrelated to ESLint.

There’s also a VSCode extension.

Refer to Rome’s Getting Started guide, but note that you might get into conflicts between Prettier, ESLint and Rome if you want to keep all three VSCode extensions enabled.

Reviewed by Kaspars Žarinovs

Discuss on Twitter Discuss on Hacker News

Related articles: