From TSLint to ESLint, or How I Learned to Lint GraphQL Code

By Christopher Pappas

At the beginning of January we discovered an interesting note in TypeScript's roadmap about linting:

In a survey we ran in VS Code a few months back, the most frequent theme we heard from users was that the linting experience left much to be desired. Since part of our team is dedicated to editing experiences in JavaScript, our editor team set out to add support for both TSLint and ESLint. However, we noticed that there were a few architectural issues with the way TSLint rules operate that impacted performance. Fixing TSLint to operate more efficiently would require a different API which would break existing rules (unless an interop API was built like what wotan provides).

Meanwhile, ESLint already has the more-performant architecture we're looking for from a linter. Additionally, different communities of users often have lint rules (e.g. rules for React Hooks or Vue) that are built for ESLint, but not TSLint.

Given this, our editor team will be focusing on leveraging ESLint rather than duplicating work. For scenarios that ESLint currently doesn't cover (e.g. semantic linting or program-wide linting), we'll be working on sending contributions to bring ESLint's TypeScript support to parity with TSLint. As an initial testbed of how this works in practice, we'll be switching the TypeScript repository over to using ESLint, and sending any new rules upstream.

At Artsy we've been using TSLint for a few years now; it's worked well for us, and we've even written our own custom rules. However, given the vastness of the JS ecosystem and how fast it moves, it's easy to recognize this announcement as an exciting moment for tooling simplicity.

To give an example, anyone who has built a culture around Airbnb's JavaScript style guide will instantly recognize the conundrum they're in when migrating to TypeScript:

a reddit user discovers their linting rules no longer work

This means that teams maintaining legacy JavaScript codebases will no longer have to also maintain two versions of often nearly identical rule-sets. All of the aggregate culture that builds up around linting can now be shared in a forward and backward facing way, making the often-daunting process of migrating a codebase from JavaScript to TypeScript a much easier sell.

With this in mind we wanted to give the new officially-sanctioned typescript-eslint project a spin and document our findings.

Setup

To get started, install the necessary dependencies:

1
$ yarn install -D eslint typescript @typescript-eslint/eslint-plugin

Then create a new .eslintrc.js and add a bit of setup:

1
2
3
4
5
6
7
8
9
10
module.exports = {
  parser: "@typescript-eslint/parser",
  plugins: ["@typescript-eslint"],
  extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  parserOptions: {
    ecmaVersion: 6,
    project: "./tsconfig.json",
    sourceType: "module"
  }
}

Note that parserOptions.project points to your tsconfig.json file:

1
2
3
{
  "compilerOptions": {}
}

Next, add a bit of TypeScript to a file

1
$ echo "export const foo: any = 'bar'" > index.ts

and run the linter:

1
2
3
4
5
6
$ yarn eslint . --ext .ts,.tsx

~/index.ts
  1:12  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any

1 problem (0 errors, 1 warnings)

Very nice!

Now lets expand the example a bit and add something more sophisticated, which in Artsy's use-case is commonly GraphQL:

1
$ yarn add -D eslint-plugin-graphql graphql-tag apollo

Update tsconfig.json and let it know we'll be using node for imports:

1
2
3
4
5
{
  "compilerOptions": {
    "moduleResolution": "node"
  }
}

In .eslintrc.js add these rules (while noting the addition of graphql to plugins and graphql/template-strings under rules):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require("path")

module.exports = {
  parser: "@typescript-eslint/parser",
  plugins: ["@typescript-eslint", "graphql"],
  extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  parserOptions: {
    ecmaVersion: 6,
    project: "./tsconfig.json",
    sourceType: "module"
  },
  rules: {
    "graphql/template-strings": [
      "error",
      {
        schemaJsonFilepath: path.resolve(__dirname, "./schema.json"),
        tagName: "graphql"
      }
    ]
  }
}

For GraphQL to know what to lint, we'll need a schema. Thankfully the Ethiopian Movie Database has our back :)

1
2
3
4
$ yarn apollo service:download --endpoint https://etmdb.com/graphql
  ✔ Loading Apollo Project
  ✔ Saving schema to schema.json
✨  Done in 2.18s.

Back in index.ts, add this bit of code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import graphql from "graphql-tag"

export const MovieQuery = graphql`
  query MoveQuery {
    allCinemaDetails(before: "2017-10-04", after: "2010-01-01") {
      edges {
        nodez {
          slug
          hallName
        }
      }
    }
  }
`

And run the linter:

1
2
3
4
5
6
$ yarn eslint . --ext .ts,.tsx

~/index.ts
  7:9  error  Cannot query field "nodez" on type "CinemaDetailNodeEdge". Did you mean "node"?  graphql/template-strings

1 problem (1 error, 0 warnings)

Ahh yes, I meant node.

Bonus: VSCode Integration

As developers, we like our tools to work for us, and in 2019 the tool that seems to do that best just happens to be a brilliant open source product from Microsoft. There were a couple unexpected configuration issues when we were setting this up, but thankfully they're easy fixes.

1
$ mkdir .vscode && touch .vscode/settings.json

Then add a couple settings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "editor.formatOnSave": true,
  "eslint.autoFixOnSave": true,
  "eslint.validate": [
    {
      "language": "javascript",
      "autoFix": true
    },
    {
      "language": "javascriptreact",
      "autoFix": true
    },
    {
      "language": "typescript",
      "autoFix": true
    },
    {
      "language": "typescriptreact",
      "autoFix": true
    }
  ],
  "tslint.enable": false
}

Format on save, fix on save, autofix on save, tell ESLint to recognize .ts (and .tsx, for the React folks) then disable tslint so that eslint can do its thing:

eslint displaying graphql error in VSCode IDE

Now ESLint will show you right where your GraphQL error is from within VSCode. Pretty sweet.

Be sure to read The future of TypeScript on ESLint for more details.