Set Up An ExpressJS Application With Typescript, Eslint & Prettier

Reading Time: 15 minutes

Have you ever been struggled with how to set up your NodeJS project with Typescript, Eslint, and Prettier? Or did you end up googling “Typescript/Eslint/Prettier configuration files” for every single project and blindly using them without knowing exactly what each setting does?

Well, in today’s post, I will guide you through exactly how I’d configure Typescript, Eslint, and Prettier in my own projects. After this post, you will not only understand the configuring process but also the reason behind every single setting that we put in there.

For demonstration purpose, together we will create an ExpressJS application. Since it’s so simple to set up, we can focus entirely on how to configure Typescript, Eslint, and Prettier. The GitHub URL for this project is here.

Let’s get started!

Configure ExpressJS application with Typescript

First, let’s create a folder called server:

mkdir server && cd server

Then we will initialize a new NodeJS project like below:

yarn init -y

Next, let’s create a new file called index.ts.

.ts? Hmm…

As I said above, that is because we will implement the whole application using Typescript.

mkdir src
touch src/index.ts

Let’s fill the file with those lines of code below:

// src/index.ts

const main = (name: string): void => {
  console.log(`hello, ${name}`);
}

main("Trung");

Before we introduce any ExpressJS stuff, let’s figure how to execute that code first. Since it’s a Typescript file, we can’t simply run it with node src/index.ts. (If the content of a .ts file is just normal Javascript code though, we CAN run it directly with node)

We have two options here:

  • Use a Typescript version of node: ts-node
  • Compile Typescript code to Javascript with tsc and execute the generated Javascript script

We will go with option 2. You can try out the first option later by installing ts-node: yarn add -D ts-node then ts-node src/index.ts.

We will need to install one dependency first which is typescript.

yarn add -D typescript

We installed typescript as devDependencies because we won’t use Typescript code in production. Instead, we will get them compiled to Javascript code first and deploy the generated Javascript code.

Next, let’s use tsc to compile the Typescript code:

yarn tsc

We notice that one new file called index.js was created inside the same src folder. Let’s examine the content of it:

var main = function (name) {
    console.log("hello, " + name);
};
main("Trung");

That is definitely legit Javascript code! We can safely assume that we can run that file with node now:

node src/index.js

// output
hello, Trung

And that’s how we compile and run a Typescript file. Not so hard, right?

Obviously, configuring a proper Typescript project requires much more than that. I bet you have seen or been using a gigantic Typescript config file and it’s really hard to fully understand why you need that in the first place. That’s why I think it’s worth our time to gradually build up your own configuration for your Typescript project. You may end up with a much more compact config file or still use the current config but with a deeper understanding of what each line does.

Alright, as you can see, the generated Javascript code was created in the same folder, which clearly, is not very good. Let’s tell Typescript to generate them somewhere else, say, a folder named dist.

One way to tell the Typescript compiler is via a file called tsconfig.json. Let’s create that file below the root folder (e.g. ./server/tsconfig.json).

touch tsconfig.json

Let’s fill the file like below to set the output directory for generated Javascript code:

// tsconfig.json

{
  "compilerOptions": {
    "outDir": "./dist"
  }
}

Now if we run yarn tsc again, the compiler will generate index.js under the dist folder. (If that folder didn’t exist, the compiler will create it for us)

We can confirm that the content of index.js is still the same. Great! Let’s add a few more settings.

Because it’s very likely that we are going to use a lot of ES6 syntaxes (module imports & exports, array/object spreading, etc). It’s a good idea to specify that inside tsconfig.json.

// tsconfig.json

{
  "compilerOptions": {
    "target": "ES6", // code is written with ES6 syntaxes
    "outDir": "./dist" // output folder for compiled files
  }
}

Let’s compile again. You may now notice that we have to run yarn tsc everytime and it’s kind of annoying. Typescript has a -w flag so that it can watch for the changes of Typescript code and automatically re-compile those which have been updated.

Let’s create some script commands in package.json: watch and dev. We can open a terminal window and run yarn watch, then open a new one and run yarn dev to execute the generated Javascript code.

// package.json
...
  "scripts": {
    "watch": "tsc -w",
    "dev": "node dist/index.ts"
  },
...

Now you may have noticed something strange. The index.js file doesn’t look the same anymore, instead, it looks like below:

// dist/index.js

const main = (name) => {
    console.log(`hello, ${name}`);
};
main("Trung");

Hmm…, interesting! The compiler kept the fat-arrow format of ES6. How is that even possible?

If you go check Typescript compiler options, you will see that Typescript use --modules option to decide how to generate Javascript code. If --target is ES6, then --modules with be set to ES6 as well. And that’s is not what we want.

The reason behind that is NodeJS doesn’t guarantee to use the latest Javascript features, so as a rule of thumb, we should explicitly tell Typescript to generate Javascript code using CommonJS (this may change in the future, who know!). That way, we can be sure that nothing strange is going to happen.

// tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES6",
    "outDir": "./dist"
  }
}

Next, let’s tell the compiler what to compile and what to not compile:

{
  "compilerOptions": {
    ...
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "./src/**/*.tsx",
    "./src/**/*.ts"
  ]
}

That will do the job for now. We will add more configurations along the way. Let’s go ahead and set up a web application with ExpressJS.

yarn add express

Then, modify the content of src/index.ts as follows:

// src/index.ts

import express from "express";

const main = (): void => {
  const app = express();

  app.listen(8000, () => {
    console.log("server is listening at http://localhost:8000");
  })
}

main();

Now if we look at the yarn watch terminal, we would see nothing wrong and index.ts still got compiled successfully. But when we try to run it with yarn dev, an error like below would occur:

yarn dev

// output
...
    const app = express_1.default();
                                 ^

TypeError: express_1.default is not a function
...

Now you’re starting to hate Typescript, aren’t you? 😉

Well, this kind of error happens when Typescript doesn’t have information about the types of the module we’re trying to use. How can we get that information? We can get it like a normal package, and those special packages mostly belong to a group called @types.

What does that mean? Let’s say we want to use express with Typescript. Then there will be (most likely) another package called @types/express which provides the necessary types for the original express package. (Some packages don’t necessarily follow this convention, but instead include their own types in the same package)

By the way, if you use VS Code, it will tell if you need to add types from an additional package:

Fig 1: VS Code is trying to warn that we need to install types for express

Okay, let’s install the necessary types for express:

yarn add -D @types/express

Now with the types installed, the compiler will now do the job more properly and we started to see it complain about the module importing line:

error TS1259: Module '"/Users/chun/workspace/setup-fullstack/server/node_modules/@types/express/index"' can only be default-imported using the 'esModuleInterop' flag

1 import express from "express";
         ~~~~~~~

  node_modules/@types/express/index.d.ts:116:1
    116 export = e;
        ~~~~~~~~~~~
    This module is declared with using 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.

One of the reasons I love Typescript is that the compiling error is not only so clear and informative, but it also tells us how to fix that. So according to the error, we have two options:

  • Option 1: Since express doesn’t have a default export (like most NodeJS packages), we can change the import line into this:
// src/index.ts

import * as express from "express";

Now the error went away. Most people will be okay with this solution, but some (like myself) don’t. I don’t want to do this to all the packages that I need to import, that’s when I can use the hint from the error: set the esModuleInterop flag.

  • Option 2: Inside tsconfig.json, let’s add this setting to compilerOptions object:
// tsconfig.json

{
  "compilerOptions": {
    ...,
    esModuleInterop: true
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "./src/**/*.tsx",
    "./src/**/*.ts"
  ]
}

Now we can use the import line like before: import express from "express"; and the compiler will still be happy getting the work done.

Alright, let’s yarn dev. If you see something get printed out like below, then you have successfully set up the express application.

yarn dev

// output
yarn run v1.22.4
$ node dist/index.js
server is listening at http://localhost:8000

Let’s add a default endpoint so that we can test the server from the browser:

import express from "express";

const main = (): void => {
  const app = express();

  app.get("/", (_, res) => {         // new
    res.json({status: "success"})    // new
  });                                // new

  app.listen(8000, () => {
    console.log("server is listening at http://localhost:8000");
  })
}

main();

Stop the server and run yarn dev again. Then open the browser and go to http://localhost:8000, we should see a JSON like this: {"status": "success"}. Yeah, it worked!

One other thing though, having to stop and start yarn dev every time is kind of time consuming. We can switch to use nodemon to watch for changes in dist/index.js. So now we have yarn watch watch for changes in src/index.ts and yarn dev watch for changes in dist/index.ts. All we have to do is to install nodemon

yarn add -D nodemon

and update the dev command in package.json:

// package.json

...
  "scripts": {
    "watch": "tsc -w",
    "dev": "nodemon dist/index.js"
  },
...

And that’s how I created my simple boilerplate Typescript code for ExpressJS application. You can go check the Typescript compiler options to add more rules and settings to your tsconfig.json, below is my version:

// tsconfig.json

{
  "compilerOptions": {
    "module": "CommonJS",
    "target": "ES6",
    "outDir": "./dist",
    "esModuleInterop": true,
    "sourceMap": true,
    "strictFunctionTypes": true,
    "strictNullChecks": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "removeComments": true
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "./src/**/*.tsx",
    "./src/**/*.ts"
  ]
}

Configure Eslint and Prettier

Okay, now we’re at the fun part. I hated it very much the first time I had to configure eslint few years ago. I ended up using other people’s configurations and installing a list of dependencies without knowing what each is for. I also tried to use eslint --init as suggested by Eslint itself but in my case, it didn’t help much either.

So I decided to experiment on my own to come up with my own configuration. But in order to do that, I needed to have a clear goal: what do I want from using eslint? (And I think that is the question we as developers should ask ourselves every time we decide to use something)

As funny as it may sound, I had to ask myself again: what can eslint do for me? It turned out, there are two main things that eslint can shine: code syntax checking and code style enforcement.

How are they different? Let me explain:

  • Code syntax checking

Within eslint, we can have a set of rules on how to evaluate the code we wrote. Some code writing styles are considered bad practice/inefficient or just simply there are other ways to do the exact same thing using new features of ES6 such as object spreading etc.

For example: using var is no longer a recommended way to define variables. eslint no-vars rule will raise an error and tell us to change to const/let instead.

// error by no-vars rule
var a = 0;
var b = a + 1;

// no error by no-vars rule
let a = 0;
let b = a + 1;

Let’s take a look at another example that I took from Airbnb Style Guide (more about what it is later).

// bad
const original = { a: 1, b: 2 };
const copy = Object.assign({}, original, { c: 3 }); // copy => { a: 1, b: 2, c: 3 }

// good
const original = { a: 1, b: 2 };
const copy = { ...original, c: 3 }; // copy => { a: 1, b: 2, c: 3 }

First and foremost, the code snippet that was labeled as bad is not bad in terms of programming. copy was created in a way that we didn’t mutate original. That is extremely important in the functional programming world, where we’re not supposed to modify the input variables.

So, why it was labeled as bad? Because with ES6’s new syntax, we now have a better and more compact way to do the same thing and the prefer-object-spread rule can help us spot the old way of writing codes out and favor the newer one.

That does sound like a lot of rules to remember! Yes, it does. But luckily, there are predefined well-known sets of rules that we can choose from: Standard, Airbnb, or Google. I prefer using rules from Airbnb, but feel free to experiment with others as well.

  • Code style enforcement

Unlike code syntax checking, code style enforcement cares more about how the code looks. Whether the code looks good or bad depends on each individual’s preference and like code syntax checking, it’s evaluated based on a specific set of rules.

For example, some people don’t like adding colons to Javascript codes, but I do. So to me, lines ending with colons look good and I can set some rule to force each line to end with a colon.

For another example, let’s look at the React code below and how it should be changed to look good:

// bad

import React from 'react';

const SomeComponent = () => (
  <div>Hey there!</div>
)

export default SomeComponent

// good
import React from "react";

const Test = () => <div>Hey there!</div>;

export default Test;

So that sounds like another bunch of rules to type in, please tell me there are some presets too! Yes, there are. In fact, well-known eslint configurations like Airbnb do include a few styling rules too. But probably the favorite config of most developers when it comes to styling is Prettier. It gives us a lot of styling rules that we can apply right away or customize for our own needs.

And that’s the two things that in my opinion eslint really excels and if utilized properly, it can help us write much better and maintainable Javascript/Typescript codes.

So that’s what eslint can do. What I want from eslint is basically what eslint can already do for me 🤣. Oh I forgot, it also has to be able to work with Typescript codes too.

Let me show you how I configure eslint in my project. It may not be the best way to do it but hey, it worked for me.

The first thing we need to do is to install just eslint to devDependencies:

yarn add -D eslint

Next, we will create a config file for eslint and I prefer the Javascript format:

touch .eslintrc.js

Let’s fill something in. Since we are working with Typescript code, below is the bare minimum setting that we ought to add:

// .eslintrc.js

module.exports = {
  root: true,
  parser: "@typescript-eslint/parser", // for parsing Typescript code
  extends: [
    "airbnb"                           // to use airbnb set of rules
  ],
  rules: {}                            // our own set of customized rules
}

You may notice that we’re using only Airbnb config. That’s intentional because I want to show you that the order of extensions matters and that Airbnb actually has some styling rules too.

Okay, let’s go linting! To run eslint we need to run the following command:

yarn eslint . --ext .ts,.tsx

The --ext flag tells eslint to look for files ending with specified extensions which is either .ts or .tsx in this situation. We can also add that command in to package.json:

// package.json

{
  ...
  "scripts": {
    "watch": "tsc -w",
    "dev": "nodemon dist/index.js",
    "lint": "eslint . --ext .ts,.tsx"  // new
  },
  ...
}

Now let’s open a terminal and run yarn lint.

yarn lint

// output
yarn run v1.22.4
$ eslint . --ext .ts,.tsx

Oops! Something went wrong! :(

ESLint: 7.15.0

ESLint couldn't find the config "airbnb" to extend from. Please check that the name of the config is correct.

The config "airbnb" was referenced from the config file in "/Users/chun/workspace/setup-fullstack/server/.eslintrc.js".

If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.

error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Now you understand why I only installed eslint and nothing else. I know what ruleset I want to apply and when I run yarn lint, eslint will tell me exactly what I should install. That’s my dark magic trick!

In this case we need to install a eslint config from airbnb, and the package to install is called eslint-config-airbnb. Let’s go ahead and grab it.

yarn add -D eslint-config-airbnb

Run yarn lint again. It’s gonna complain about something else. This time it tells us that we don’t have @typescript-eslint/parser installed. Okay, let’s grab it too.

yarn add -D @typescript-eslint/parser

You got the idea. If you run yarn lint again, eslint will give an error again about another uninstalled package. That is because Airbnb config depends on other packages so we need to install them all. The easiest way is to install them one by one until it’s happy! 😉

To save you some time, here’s a full list of dependencies we need to install if we keep hitting yarn lint. You can install them all with one command:

yarn add -D eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-import

Now if we run yarn lint again, we will see a bunch of errors which didn’t come from missing dependencies but index.ts. Great! eslint is working now.

/Users/chun/workspace/setup-fullstack/server/src/index.ts
   1:21  error    Strings must use singlequote    quotes
   6:11  error    Strings must use singlequote    quotes
   7:14  error    A space is required after '{'   object-curly-spacing
   7:23  error    Strings must use singlequote    quotes
   7:32  error    A space is required before '}'  object-curly-spacing
   7:34  error    Missing semicolon               semi
  11:5   warning  Unexpected console statement    no-console
  11:17  error    Strings must use singlequote    quotes
  12:5   error    Missing semicolon               semi
  13:2   error    Missing semicolon               semi

✖ 10 problems (9 errors, 1 warning)
  9 errors and 0 warnings potentially fixable with the `--fix` option.

As you can see, eslint did print out a lot of errors, most of which are styling errors! That is because the code is too simple to have any syntax errors and Airbnb does have some styling rules as well.

Let’s introduce some naughty code and see if Airbnb config can catch them:

// src/index.ts

import express from "express";

const main = (): void => {
  let a: any = {"k1": "v1", k2: "v2"}                 // new
  var b = Object.assign({}, a, {"k3": "v3" })   // new
  console.log(b);                                // new

  const app = express();

  app.get("/", (_, res) => {
    res.json({status: "success"})
  });

  app.listen(8000, () => {
    console.log("server is listening at http://localhost:8000");
  })
}

main();

And here’s a long list of errors that we was waiting for:

   1:21  error    Strings must use singlequote                                      quotes
   4:7   error    'a' is never reassigned. Use 'const' instead                      prefer-const
   4:11  error    A space is required after '{'                                     object-curly-spacing
   4:12  error    Unnecessarily quoted property 'k1' found                          quote-props
   4:12  error    Strings must use singlequote                                      quotes
   4:18  error    Strings must use singlequote                                      quotes
   4:28  error    Strings must use singlequote                                      quotes
   4:32  error    A space is required before '}'                                    object-curly-spacing
   4:33  error    Missing semicolon                                                 semi
   5:3   error    Unexpected var, use let or const instead                          no-var
   5:11  error    Use an object spread instead of `Object.assign` eg: `{ ...foo }`  prefer-object-spread
   5:32  error    A space is required after '{'                                     object-curly-spacing
   5:33  error    Unnecessarily quoted property 'k3' found                          quote-props
   5:33  error    Strings must use singlequote                                      quotes
   5:39  error    Strings must use singlequote                                      quotes
   5:46  error    Missing semicolon                                                 semi
   6:3   warning  Unexpected console statement                                      no-console
  10:11  error    Strings must use singlequote                                      quotes
  11:14  error    A space is required after '{'                                     object-curly-spacing
  11:23  error    Strings must use singlequote                                      quotes
  11:32  error    A space is required before '}'                                    object-curly-spacing
  11:34  error    Missing semicolon                                                 semi
  15:5   warning  Unexpected console statement                                      no-console
  15:17  error    Strings must use singlequote                                      quotes
  16:5   error    Missing semicolon                                                 semi
  17:2   error    Missing semicolon                                                 semi

✖ 26 problems (24 errors, 2 warnings)
  24 errors and 0 warnings potentially fixable with the `--fix` option.

eslint with Airbnb config did quite a good job, didn’t it? Before introducing Prettier for styling rules, I would like to add some more syntax rulesets that are often used together with Airbnb and Typescript:

  • eslint:recommended
  • plugin:@typescript-eslint/recommended

The first one is pretty straightforward, you may guess it consists of rules recommended by eslint itself. What about the other one?

As the name suggested, it contains rules for Typescript codes and they came from a special package type called plugin. Before diving into what plugins actually are, let’s add those three into .eslintrc.js, right above airbnb like this:

module.exports = {
  root: true,
  parser: "@typescript-eslint/parser",
  extends: [
    "eslint:recommended",                                 // new
    "plugin:@typescript-eslint/recommended",              // new
    "airbnb",
  ],
  rules: {}
}

Now, if we try to run yarn lint, it will tell us to install a plugin called @typescript-eslint/eslint-plugin. Let’s install that package:

yarn add -D @typescript-eslint/eslint-plugin

Run yarn lint again and we should see a list of errors like before, plus one new warning about the explicit use of any for types, which came from the plugin we just installed.

   ...
   4:10  warning  Unexpected any. Specify a different type                          @typescript-eslint/no-explicit-any
   ...

Now we have a fairly good code syntax checking configuration. What if I don’t like some rules and want to turn only those off or simply change from error to warning and vice versa?

That where we need to use the rules object in .eslintrc.js. Let’s say I want to turn off prefer-object-spread and @typescript-eslint/no-explicit-any. Here’s how we tell eslint.

// .eslintrc.js

module.exports = {
  root: true,
  parser: "@typescript-eslint/parser",
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "airbnb",
  ],
  rules: {
    "prefer-object-spread": 0,                        // new
    "@typescript-eslint/no-explicit-any": "off"       // new
  },
}

Run yarn lint again and we can see that the error of prefer-object-spread and the warning of @typescript-eslint/no-explicit-any have gone. I also showed you two ways to set the rule’s value: either string (off, warning, error) or integer (0, 1, 2).

Now let’s undo the changes we have just made because we need those two important rules. You may have noticed we have a fair amount of styling errors, but instead of relying on Airbnb on styling, as discussed earlier, we’d better leave that job for Prettier.

Let’s extends prettier in .eslintrc.js:

// .eslintrc.js

module.exports = {
  root: true,
  parser: "@typescript-eslint/parser",
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier",                       // to use prettier set of rules
    "airbnb"                           
  ],
  rules: {}                      
}

We also need to install eslint-config-prettier. You know the trick now, don’t you? 😉

yarn add -D eslint-config-prettier

Let’s run yarn lint again:

yarn lint

// output
   1:21  error    Strings must use singlequote                                      quotes
   4:7   error    'a' is never reassigned. Use 'const' instead                      prefer-const
   4:10  warning  Unexpected any. Specify a different type                          @typescript-eslint/no-explicit-any
   4:16  error    A space is required after '{'                                     object-curly-spacing
   4:17  error    Unnecessarily quoted property 'k1' found                          quote-props
   4:17  error    Strings must use singlequote                                      quotes
   4:23  error    Strings must use singlequote                                      quotes
   4:33  error    Strings must use singlequote                                      quotes
   4:37  error    A space is required before '}'                                    object-curly-spacing
   4:38  error    Missing semicolon                                                 semi
   5:3   error    Unexpected var, use let or const instead                          no-var
   5:11  error    Use an object spread instead of `Object.assign` eg: `{ ...foo }`  prefer-object-spread
   5:32  error    A space is required after '{'                                     object-curly-spacing
   5:33  error    Unnecessarily quoted property 'k3' found                          quote-props
   5:33  error    Strings must use singlequote                                      quotes
   5:39  error    Strings must use singlequote                                      quotes
   5:46  error    Missing semicolon                                                 semi
   6:3   warning  Unexpected console statement                                      no-console
  10:11  error    Strings must use singlequote                                      quotes
  11:14  error    A space is required after '{'                                     object-curly-spacing
  11:23  error    Strings must use singlequote                                      quotes
  11:32  error    A space is required before '}'                                    object-curly-spacing
  11:34  error    Missing semicolon                                                 semi
  15:5   warning  Unexpected console statement                                      no-console
  15:17  error    Strings must use singlequote                                      quotes
  16:5   error    Missing semicolon                                                 semi
  17:2   error    Missing semicolon                                                 semi

✖ 27 problems (24 errors, 3 warnings)
  24 errors and 0 warnings potentially fixable with the `--fix` option.

We saw the same as before! Why was that?

Because the order in which we extend configurations does matter. Right now, Airbnb is listed after Prettier, which means that all styling rules from Airbnb will overwrite the ones from Prettier! So a good rule of thumbs is to always put Prettier at the end of the list of extensions to prioritize the styling rules from it.

// .eslintrc.js

module.exports = {
  root: true,
  parser: "@typescript-eslint/parser",
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "airbnb",
    "prettier",                       // to use prettier set of rules                     
  ],
  rules: {}                      
}

Now let’s run yarn lint one more time. The result may surprise you:

yarn lint

// output
   4:7   error    'a' is never reassigned. Use 'const' instead                      prefer-const
   4:10  warning  Unexpected any. Specify a different type                          @typescript-eslint/no-explicit-any
   5:3   error    Unexpected var, use let or const instead                          no-var
   5:11  error    Use an object spread instead of `Object.assign` eg: `{ ...foo }`  prefer-object-spread
   6:3   warning  Unexpected console statement                                      no-console
  15:5   warning  Unexpected console statement                                      no-console

✖ 6 problems (3 errors, 3 warnings)
  3 errors and 0 warnings potentially fixable with the `--fix` option.

The list of errors did change, which is good. But all styling errors were gone! What have eslint-config-prettier done to me?

Just kidding, if we head to eslint-config-prettier‘s Github, this is the first thing they tell us:

eslint-config-prettier turns off all rules that are unnecessary or might conflict with Prettier.

That makes so much sense. Since Airbnb or Google rulesets may include some styling enforcement, the wisest thing to do is to turn them all off.

But extending prettier is not enough? As stated on eslint-config-prettier‘s GitHub, one should extend some plugins that other configs are using. What is that supposed to mean?

Let’s take a look at our package.json real quick:

// package.json

...
  "devDependencies": {
    "@types/express": "^4.17.9",
    "@typescript-eslint/eslint-plugin": "^4.9.0",
    "@typescript-eslint/parser": "^4.9.0",
    "eslint": "^7.15.0",
    "eslint-config-airbnb": "^18.2.1",
    "eslint-config-prettier": "^7.0.0",
    "eslint-plugin-import": "^2.22.1",
    "eslint-plugin-jsx-a11y": "^6.4.1",
    "eslint-plugin-react": "^7.21.5",
    "nodemon": "^2.0.6",
    "typescript": "^4.1.2"
  },
...

As we can see, there are three plugins installed (all of them are required by eslint-config-airbnb): eslint-plugin-import, eslint-plugin-jsx-a11y, and eslint-plugin-react. Do we have to extend them all? No, we don’t. Then how do we know which one to put to extends then?

Well, if we take a look at eslint-config-prettier‘s GitHub again:

Fig 2: plugins that need to be put to extends array

We can see that there are just a couple of plugins that we need to care about. So, can you guess from the names what we need to add? Yes, they are react (react plugin is being used by airbnb config) and @typescript-eslint (so obvious). And the naming convention is prettier/<plugin_name>. Let’s update our .eslintrc.js as follows:

// .eslintrc.js

...
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "airbnb",
    "prettier",
    "prettier/react",
    "prettier/@typescript-eslint",
  ],
...

Run yarn lint again to make sure we didn’t break anything.

Then how can we specify the styling rules then? By using plugins.

In fact, I was kind of reluctant to address the following question until now: what is the difference between configs (eslint-config-<something> which we put inside extends array) and plugins (eslint-plugin-<something> that we installed and are about to use).

There was an excellent answer on StackOverflow already and I suggest you guys to have a look later on.

In short, when you put eslint-config-<something> in the extends array inside .eslintrc.js, it’s like you’re copying the whole rules object defined inside that package. To give you a concrete example, if we dig into the repo of @typescript-eslint, you will find a file for @typescript-eslint/recommended and it’s basically exporting a rules object:

// typescript-eslint/packages/eslint-plugin/src/configs/recommended.ts

export = {
  extends: ['./configs/base', './configs/eslint-recommended'],
  rules: {
    '@typescript-eslint/adjacent-overload-signatures': 'error',
    '@typescript-eslint/ban-ts-comment': 'error',
    '@typescript-eslint/ban-types': 'error',
    '@typescript-eslint/explicit-module-boundary-types': 'warn',
    'no-array-constructor': 'off',
    '@typescript-eslint/no-array-constructor': 'error',
    'no-empty-function': 'off',
    '@typescript-eslint/no-empty-function': 'error',
    '@typescript-eslint/no-empty-interface': 'error',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-extra-non-null-assertion': 'error',
    'no-extra-semi': 'off',
    '@typescript-eslint/no-extra-semi': 'error',
    '@typescript-eslint/no-inferrable-types': 'error',
    '@typescript-eslint/no-misused-new': 'error',
    '@typescript-eslint/no-namespace': 'error',
    '@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
    '@typescript-eslint/no-non-null-assertion': 'warn',
    '@typescript-eslint/no-this-alias': 'error',
    'no-unused-vars': 'off',
    '@typescript-eslint/no-unused-vars': 'warn',
    '@typescript-eslint/no-var-requires': 'error',
    '@typescript-eslint/prefer-as-const': 'error',
    '@typescript-eslint/prefer-namespace-keyword': 'error',
    '@typescript-eslint/triple-slash-reference': 'error',
  },
};

That’s how we get the rules from a specific eslint config. plugins, on the other hand, can have one or both below:

  • a collection of configs
  • a collection of rules

Take a look at @typescript-eslint/recommended, that is a config(since we can add to extends array), but it came from a plugin called @typescript-eslint/eslint-plugin.

plugins can also contain a bunch of rules which we can use inside our own rules object. One important note though, unless we specifically use some rules from a plugin (by adding to rules object), by default all the rules defined inside a plugin won’t have any effect at all.

That being said, we will need prettier plugin to use its styling rulesets. Let’s go ahead and install it:

yarn add -D eslint-plugin-prettier

To use prettier‘s styling rules, the simplest way is to add prettier to plugins array and pull the prettier/prettier ruleset from the plugin and add it in the rules object inside .eslintrc.js:

module.exports = {
  root: true,
  parser: "@typescript-eslint/parser",
  plugins: ["prettier"],                       // tell eslint to use prettier plugin
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "airbnb",
    "prettier",
    "prettier/react",
    "prettier/@typescript-eslint",
  ],
  rules: {
    "prettier/prettier": "error",              // use all prettier/prettier rules
}

Now let’s check the output of yarn lint.

Error, isn’t it? 😉 We need to install a separate package called prettier:

yarn add -D prettier

We’re now all set. Run yarn lint again:

yarn lint

// output
   4:7   error    'a' is never reassigned. Use 'const' instead                      prefer-const
   4:10  warning  Unexpected any. Specify a different type                          @typescript-eslint/no-explicit-any
   4:17  error    Replace `"k1":·"v1",·k2:·"v2"}` with `·k1:·"v1",·k2:·"v2"·};`     prettier/prettier
   5:3   error    Unexpected var, use let or const instead                          no-var
   5:11  error    Use an object spread instead of `Object.assign` eg: `{ ...foo }`  prefer-object-spread
   5:33  error    Replace `"k3":·"v3"·})` with `·k3:·"v3"·});`                      prettier/prettier
   6:3   warning  Unexpected console statement                                      no-console
  11:15  error    Replace `status:·"success"})` with `·status:·"success"·});`       prettier/prettier
  15:5   warning  Unexpected console statement                                      no-console
  16:5   error    Insert `;`                                                        prettier/prettier
  17:2   error    Insert `;`                                                        prettier/prettier

✖ 11 problems (8 errors, 3 warnings)
  8 errors and 0 warnings potentially fixable with the `--fix` option.

There we go, now all styling errors only came from prettier/prettier. Cool! Not so hard at all, right? I told you!

Next, I would like to show you my complete .eslintrc.js. It may feel like magic like “How did I even know to put them there?”. But it’s not. My approach is to use a pretty strict set of rules at the beginning and turn off any errors that I found irrelevant to my use case. I also learned from others’ config file and copied what I found useful 😉 (for example, Wes Bos has a great .eslintrc.js that you can study from).

// .eslintrc.js

module.exports = {
  root: true,
  parser: "@typescript-eslint/parser",
  plugins: ["@typescript-eslint", "prettier"],
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "airbnb",
    "prettier",
    "prettier/react",
    "prettier/@typescript-eslint",
  ],
  settings: {
    "import/resolver": {
      node: {
        extensions: [".js", ".ts"],
      },
    },
  },
  rules: {
    "import/extensions": 0,
    "import/prefer-default-export": 0,
    "class-methods-use-this": 0,
    "no-underscore-dangle": 0,
    "comma-dangle": 0,
    "max-classes-per-file": 0,
    "no-console": 0,
    "@typescript-eslint/no-non-null-assertion": 0,
    quotes: [
      2,
      "double",
      {
        avoidEscape: true,
        allowTemplateLiterals: true,
      },
    ],
    "prettier/prettier": [
      "error",
      {
        trailingComma: "es5",
        printWidth: 80,
        endOfLine: "auto",
      },
    ],
  },
};

As you may ask, the import/resolver setting is there so that eslint won’t complain when you import files from other folders. I believe that Javascript files (.js) don’t require that to be specifically set, but Typescript files (.ts) definitely do.

There is one more thing that is also important. We need to tell eslint what files/folders it should not check. Checking unnecessary files does take a lot of time and makes the eslint output hard to look at. This is especially true if you only work with Javascript files. Can you think of something that may be disastrous? Yes, node_modules folder!

Just like Git or Docker, we can tell eslint to ignore files/folders via a .eslintignore file. In this tiny project, we only need to ignore node_modules (this is a must for every project!) and the dist folder.

// .eslintignore

node_modules
dist

Auto-formatting

Now that we have eslint print out all syntax and styling errors. What’s next? I don’t think anybody would have time to fix each error by hand, right?

There are two ways we can get our code automatically fixed most of the errors:

  • Add --fix flag when running yarn lint
  • Configure autoformat when saving a file in VS Code

I recommend you use both of them. In my case, when I have to use some kind of boilerplate code (such as create-react-app etc), I would use the first option because obviously, that’s the quickest way. Then during the development, I will rely on VS Code’s autoformat functionality to fix eslint‘s errors every time I save a file.

So, how do we configure autoformat in VS Code, though? My suggestion is to do the following step to every project, rather than going through VS Code super long preference settings.

  • Create a new folder name .vscode, this folder is used to store settings for the current workspace (i.e. project)
mkdir .vscode
  • Then create a new file under .vscode folder called settings.json:
touch .vscode/settings.json
  • Finally, fill the following lines into the newly created file:
// .vscode/settings.json

{
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  }
}

Now, if you either run yarn lint --fix or hit Ctrl-S on ./src/index.ts, you will notice that the content got fixed automatically according to eslint‘s errors we saw above:

// ./src/index.ts

// Before
import express from "express";

const main = (): void => {
  let a: any = {"k1": "v1", k2: "v2"}                
  var b = Object.assign({}, a, {"k3": "v3" })  
  console.log(b);                              

  const app = express();

  app.get("/", (_, res) => {
    res.json({status: "success"})
  });

  app.listen(8000, () => {
    console.log("server is listening at http://localhost:8000");
  })
}

main();

// After
import express from "express";

const main = (): void => {
  const a: any = { k1: "v1", k2: "v2" };
  const b = { ...a, k3: "v3" };
  console.log(b);

  const app = express();

  app.get("/", (_, res) => {
    res.json({ status: "success" });
  });

  app.listen(8000, () => {
    console.log("server is listening at http://localhost:8000");
  });
};

main();

That was amazing, isn’t it? Although there are some warnings that can’t be fixed automatically, such as no-console or no-explicit-any, but those are things that we will delete eventually (console.log statements) or rather fix by ourselves (the use of type any). Imagine dealing with a much bigger codebase, the benefit we can get from using Typescript, Eslint, and Prettier will escalate very quickly. Believe me!

Conclusions

In today’s post, we have gone through the process of setting up a simple ExpressJS project with Typescript, Eslint, and Prettier. The size of the project is pretty small, but it can save as a solid base for us to write any big applications with more compact, precise, and most importantly, maintainable codes. Thank you all for sticking with me until this point. I hope you found the post helpful. If you have any questions, don’t hesitate to open an issue on GitHub or email me directly at trungtran@machinetalk.org. Take good care of yourselves and I’ll see you guys in the next post.

Trung Tran is a software developer + AI engineer. He also works on networking & cybersecurity on the side. He loves blogging about new technologies and all posts are from his own experiences and opinions.

Leave a reply:

Your email address will not be published.