Setting Up a Strict ESLint Config for your React + Typescript Project

In addition to green tests, there's nothing more satisfying than writing consistent and maintainable code with your team. For web projects, you probably have ESLint set up. If you're unsure or want an even stricter rule, read along.

TL;DR

.eslintrc.json
{
  "root": true,
  "extends": [
    "airbnb",
    "airbnb-typescript",
    "airbnb/hooks",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
    "plugin:prettier/recommended" // Add this if you're using prettier so it plays well with ESLint
  ],
  "ignorePatterns": [], // Add your ignores here like `"coverage/*"` or `"build/*"`
  "parserOptions": {
    "project": "./tsconfig.json"
  },
  "rules": {
    // Ordering of imports
    "import/order": [
      "warn",
      {
        "groups": [
          "builtin",
          "external",
          "internal",
          "parent",
          "sibling",
          "index",
          "object"
        ],
        "alphabetize": {
          "order": "asc"
        },
        "warnOnUnassignedImports": true
      }
    ],
    // Add this if you're using React 17+ with support for the new JSX Transform
    "react/react-in-jsx-scope": "off",
    // Extend airbnb config to allow default arguments in addition to defaultProps
    "react/require-default-props": [
      "error",
      {
        "forbidDefaultForRequired": true,
        "functions": "defaultArguments"
      }
    ],
    // Prefer type imports
    "@typescript-eslint/consistent-type-imports": "error",
    // Defaulted to warn but we want it strict
    "@typescript-eslint/no-unused-vars": "error",
    // Allow void for statements. Helpful for @typescript-eslint/no-floating-promises
    "no-void": [
      "error",
      {
        "allowAsStatement": true
      }
    ],
    // Restrict the use of React.FC and React.VFC (including the longer aliases)
    "@typescript-eslint/ban-types": [
      "error",
      {
        "types": {
          "React.FC": "React.FC is unnecessary: it provides next to no benefits and has a few downsides. (see https://github.com/facebook/create-react-app/pull/8177)",
          "React.FunctionComponent": "React.FunctionComponent is unnecessary: it provides next to no benefits and has a few downsides. (see https://github.com/facebook/create-react-app/pull/8177)",
          "React.VFC": "React.VFC and React.VoidFunctionComponent were deprecated in React 18",
          "React.VoidFunctionComponent": "React.VFC and React.VoidFunctionComponent were deprecated in React 18"
        }
      }
    ]
    // Uncomment the rules below if you find @typescript-eslint/no-unsafe-* overly strict:
    //
    // It's quite difficult to comply with @typescript-eslint/no-unsafe-* rules especially when
    // importing packages that are not typed correctly so we'll show warnings instead
    // "@typescript-eslint/no-unsafe-argument": "warn",
    // "@typescript-eslint/no-unsafe-assignment": "warn",
    // "@typescript-eslint/no-unsafe-call": "warn",
    // "@typescript-eslint/no-unsafe-member-access": "warn",
    // "@typescript-eslint/no-unsafe-return": "warn"
  }
}
Click here to use a non-Typescript config
.eslintrc.json
{
  "root": true,
  "extends": [
    "airbnb",
    "airbnb/hooks",
    "plugin:prettier/recommended" // Add this if you're using prettier so it plays well with ESLint
  ],
  "ignorePatterns": [], // Add your ignores here like `"coverage/*"` or `"build/*"`
  "rules": {
    // Ordering of imports
    "import/order": [
      "warn",
      {
        "groups": [
          "builtin",
          "external",
          "internal",
          "parent",
          "sibling",
          "index",
          "object"
        ],
        "alphabetize": {
          "order": "asc"
        },
        "warnOnUnassignedImports": true
      }
    ],
    // Add this if you're using React 17+ with support for the new JSX Transform
    "react/react-in-jsx-scope": "off",
    // Extend airbnb config to allow default arguments in addition to defaultProps
    "react/require-default-props": [
      "error",
      {
        "forbidDefaultForRequired": true,
        "functions": "defaultArguments"
      }
    ]
  }
}

Setting up

In addition to ESlint and TypeScript, we will make use of the following packages:

If you use Prettier (which I highly recommend), we'll also need to install the following:

npm install -D @typescript-eslint/eslint-plugin \
    @typescript-eslint/parser \
    eslint-config-airbnb \
    eslint-config-airbnb-typescript \
    eslint-config-prettier \
    eslint-plugin-import \
    eslint-plugin-jsx-a11y \
    eslint-plugin-prettier \
    eslint-plugin-react \
    eslint-plugin-react-hooks

Configuration

We first "extend" from the eslint config packages that we installed. The rules are extended from left to right, meaning the ordering is important:

{
  "extends": [
    "airbnb",
    "airbnb-typescript",
    "airbnb/hooks",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
    "plugin:prettier/recommended" // Add this if you're using prettier so it plays well with ESLint
  ],
}

This minimal option for parserOptions is required by Typescript ESLint for typed linting:

{
  "parserOptions": {
    "project": "./tsconfig.json"
  }
}

Summary of custom rules

There are various rules that I specified on the config like the import/order rule which will make your life much more easier if you ever feel your imports are disorganized.

-import { dateSort, getPosts } from '../lib/posts'
-import Card from '../components/Card'
 import type { InferGetStaticPropsType, NextPage } from 'next'
-import Link from 'next/link'
 import dynamic from 'next/dynamic'
+import Link from 'next/link'
+import Card from '../components/Card'
+import { dateSort, getPosts } from '../lib/posts'

If your project uses React 17+, then it's best to disable react/react-in-jsx-scope as it's enabled by default on the configs we extended from.

-import React from 'react';
-
export default function Hello({ foo = 'foo' }) {
  return <div>{foo}</div>;
}

react/require-default-props uses the default airbnb config with an additional functions option set to defaultArguments. This makes defaultProps on functional components optional as long as they have default arguments set:

function Hello({ foo = 'foo' }) {
  return <div>{foo}</div>;
}
 
Hello.propTypes = {
  foo: PropTypes.string
};

If you want to separate type imports from regular imports, consider enabling @typescript-eslint/consistent-type-imports:

import { useState } from 'react';
import type { PropsWithChildren } from 'react';

By default, the @typescript-eslint/no-unused-vars is set to warn (yellow squiggly line) and usually passes builds depending on your config. Setting it to error helps for a stricter codebase.

The no-void rule is best paired with @typescript-eslint/no-floating-promises so we can prepend void to promises that we don't need to then or catch:

// assume message.error returns a promise
function onSuccess(e) {
  void message.error(`Oh snap! {e.message}`)
}

The types React.FC and React.VFC have been widely discouraged by the community. To disallow use, add the @typescript-eslint/ban-types rule. Further discussion on React.FC can be found here.

Done

Of course, you are free to adjust the config based on your requirements. Make it your own! 🤩