loading...
Cover image for Setting up a GatsbyJS starter with TypeScript, ESLint, Prettier and pre-commit hooks

Setting up a GatsbyJS starter with TypeScript, ESLint, Prettier and pre-commit hooks

ardennl profile image Arden de Raaij Originally published at arden.nl ・Updated on ・10 min read

GatsbyJS, the static site generator on which my own blog is based, must be my favorite gateway technology. It learned me how to get comfortable with React and introduced me to GraphQL. As nowadays every project I'm working on contains TypeScript(TS), updating a Gatsby starter with TypeScript seems like a perfect way to get some in-depth practical knowledge.

In this article, we'll set up the Gatsby default starter blog with TypeScript, ESLint, Prettier and run these before every commit with lint-staged and husky.

Why TypeScript?

The answer to that question might be a blog post on its own, but this excellent StackOverflow answer from Lodewijk Bogaards will undoubtfully answer most of your questions. From the answer:

TypeScript is modern JavaScript + types. It's about catching bugs early and making you a more efficient developer, while at the same time leveraging the JavaScript community.

You had me at "catching bugs early". Let's do this!

Fork, clone and install the Gatsby blog starter

For this tutorial, I advise you to fork the gatsby blog starter to your own Github account and clone it to your local machine from there.

  • Go to https://github.com/gatsbyjs/gatsby-starter-blog.
  • Click fork
  • Clone the repository to your local machine with git clone git@github.com:<youraccount>/gatsby-starter-blog.git
  • cd into the folder
  • optional create a new branch with git checkout -b "typescript" and push
  • Run yarn install
  • Run yarn develop

Voila, your Gatsby starter is running on http://localhost:8000/ and we can start to set-up TypeScript!

Install gatsby-plugin-typescript and TypeScript

To make use of TypeScript within Gatsby, we need to add two new packages, starting with gatsby-plugin-typescript. The description page of gatsby-plugin-typescript had me slightly confused as it clearly says it does not do type-checking. So what does this plugin do exactly?

As it turns out, TypeScript in itself is a Transpiler, just like Babel. It can do both type-checking and generate several flavors of browser readable JavaScript. In GatsbyJS we only want the TypeScript type-checking though, because Gatsby already uses Babel to transpile our ESNext code.

That's why gatsby-plugin-typescript extends the GatsbyJS WebPack and Babel configurations to include the @babel/preset-typescript plugin. This way, Babel and its plugins can transpile both TypeScript and ESNext code into browser readable JS and we'll set up TypeScript independently to give us full type-checking support without compiling anything itself.

For a further explanation, I refer you to this superb article on TypeScript + Babel by Matt Turnbull.

So let's get to it and add gatsby-plugin-typescript and TypeScript to your Gatsby set-up. TypeScript can be added to the devDependencies whilst Gatsby plugins should be added as a dependency:

yarn add gatsby-plugin-typescript
yarn add typescript --dev

Don't forget to enable the plugin in the gatsby-config.js file in the root of your project:

  ...
  `gatsby-plugin-offline`,
  `gatsby-plugin-react-helmet`,
  `gatsby-plugin-typescript`,
  ...

Add and configure tsconfig.json and type-check script

Next up we'll need to add a tsconfig.json to the root of our project. TypeScript has a CLI command, tsc and when using it without specifying any files, TypeScript will always look for a tsconfig.json. If the tsconfig.json is empty, TypeScript will revert to its defaults, but we'll need to set-up a couple of things.

compilerOptions

  • "module": "commonjs" As we're using Node.js and import our NPM packages the CommonJS way, we want to make sure this option is set to commonjs
  • "target": "esnext" To be honest, I'm not sure if this does anything when we don't use TypeScript as a compiler. When we use TypeScript as a compiler we can specify the ECMA script target here. I'm still leaving it here because that's what people smarter than myself seem to do as well. In our case, we'll just target esnext.
  • "jsx": "preserve" TypeScript has a few different options for compiling JSX. Again, we're not compiling with TypeScript but when we're using JSX it will expect this option to be present. The preserve option would normally make sure the JSX code wouldn't be compiled.
  • "lib": ["dom", "esnext"] The lib option will tell TypeScript which libraries to support. This doesn't include any polyfills or anything, but will just tell TypeScript which methods are allowed when compiling and type checking. If we'd omit dom from the options and would include document.querySelector, TypeScript would show you an error.
  • "strict": true This option enables a bunch of strict type-checking options like noImplitAny, noImplicitThis and strictFunctionTypes. Go hard or go home!
  • "noEmit": true As we don't want TypeScript to create any new files because we're leaving that to the Gatsby Babel setup, it's important not to forget about this option.
  • "esModuleInterop": true, "noUnusedLocals": false Both of these options are mainly used to keep proper compatibility with Babel. You can read more about this on this article on TypeScript and Babel 7 by Microsoft.
  • "noUnusedLocals": false I don't know about you but I always have some variables hanging around for feature use. Maybe it's a bad habit and I should apply more Marie Kondo practices to my code, but not today.

Include and Exclude

We can specify both include and exclude in our config file. If there is no include specified, TypeScript will include all compatible files in the root and all subdirectories. In my case, I decided to only use the exclude option to make sure TypeScript doesn't waste time checking the compiled JavaScript in the public folder, my node_modules or my .cache directory.

Our config file should look something like this now:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "esnext",
    "jsx": "preserve",
    "lib": ["dom", "esnext"],
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "noUnusedLocals": false
  },
  "exclude": ["node_modules", "public", ".cache"]
}

Add Type-checking NPM script.

Next up, add a new script to your package.json:

"scripts": {
  ...
 "type-check": "tsc"
}

Don't worry about passing any flags. Running tsc will have TypeScript looking for our tsconfig.json which holds all our configurations. If all is well we can now run yarn type-check, which will probably result in the following error:

$ tsc
error TS18003: No inputs were found in config file '~/gatsby-starter-blog/tsconfig.json'.
Specified 'include' paths were '["**/*"]' and 'exclude' paths were '["node_modules","public",".cache"]'.

Don't worry about this! This is only because we don't have any TypeScript files in our set-up yet. All our files are still .js and seeing we haven't set allowJs to true in our tsconfig.json, there is nothing to check. We'll fix that soon enough.

Converting files to TypeScript

At this point, it's probably a good idea to start renaming your *.js files to *.ts and *.tsx (if they contain JSX). You can convert all the files in the ./src/ folder, and if your editor supports IntelliSense, it will be yelling at you with a whole bunch of red squiggly lines in no-time. At the same time, running yarn type-check should give you a whole bunch of errors, which is good for a change as it means that your configuration worked!

Normally I'd also advise you to start fixing the current type errors that TypeScript is yelling about. Because I want to make sure that you've got a complete set-up, including linting, I'm leaving the actual fixing of these errors for the follow-up blog post. For now, please bear with me as we set-up a linter and commit hooks!

Also, don't forget to commit your code and take a break!

Setting up the linters

ESLint or TSLint?

Just to prevent any confusion: The preferred linter to use with TypeScript is ESLint. You might still see a lot of tslint configuration files out there, but I believe TSLint will soon be deprecated.

Setting up ESLint and Prettier

To set up ESLint with TypeScript, Prettier and some React best practices, we'll need to add a bunch of devDependencies:

yarn add eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-prettier eslint-plugin-react --dev

Now all the necessary packages are installed, we need to add a .eslintrc.js configuration file to the root of our project (I prefer a .js file so I can add comments). Below you'll find an example of my ESLint configuration

module.exports = {
  parser: '@typescript-eslint/parser', // Specifies the ESLint parser
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier/@typescript-eslint',
    'plugin:prettier/recommended'
  ],
  settings: {
    react: {
      version: 'detect'
    }
  },
  env: {
    browser: true,
    node: true,
    es6: true
  },
  plugins: ['@typescript-eslint', 'react'],
  parserOptions: {
    ecmaFeatures: {
      jsx: true
    },
    ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
    sourceType: 'module' // Allows for the use of imports
  },
  rules: {
    'react/prop-types': 'off', // Disable prop-types as we use TypeScript for type checking
    '@typescript-eslint/explicit-function-return-type': 'off'
  },
  overrides: [
    // Override some TypeScript rules just for .js files
    {
      files: ['*.js'],
      rules: {
        '@typescript-eslint/no-var-requires': 'off' //
      }
    }
  ]
};

In this setup, the TypeScript linter will work perfectly with Prettier and ESLint while also being able to extend other ESLint settings and recommendations.

Adding Lint Scripts

To make life easier on ourselves we'll add two lint scripts to our package.json

"scripts": {
  ...
  "lint": "eslint --ignore-path .gitignore . --ext ts --ext tsx --ext js --ext jsx",
  "lint:fix": "yarn lint --fix"
}

The first script runs ESLint on every *.ts,*.js,*.tsx and *.jsx file and shows you the errors. The second one will also fix any errors that ESLint can fix on its own. If you run yarn lint now, you should see a whole bunch of lint errors in your terminal.

Setting up the editor

VSCode has excellent linting support, but to make sure we see not just the type errors but also the rules we have declared or extended in our .eslint file whilst we're coding, we need to add a bit to the VSCode settings.

"eslint.validate": [
    {
      "language": "javascript",
      "autoFix": true
    },
    {
      "language": "javascriptreact",
      "autoFix": true
    },
    {
      "language": "typescript",
      "autoFix": true
    },
    {
      "language": "typescriptreact",
      "autoFix": true
    }
  ],

You can add this to your general settings, or include it in a file in a folder labeled .vscode in the root of the project. If you want, you can download the file right here: https://github.com/aderaaij/gatsby-starter-blog/tree/typescript/.vscode

Setting up Husky and Lint Staged

Having our editor highlighting type-errors is great, but of course, the end-game is making sure that everyone who works on our code will commit code that is formatted the same and checked against the same rules. If it doesn't pass the type-checking and linting, it shouldn't be able to get added to the code-base.

For this, we'll use the NPM packages husky and lint-staged. husky allows us to run pre- and post-commit hooks and lint-staged allows us to run a linter over just the files which are being staged for a commit.
To install them, run:

yarn add husky lint-staged --dev

We can configure husky and lint-staged in our package.json or in separate files. I prefer separate files, as a glance at the file-structure can show you what goodies are already configured.

First, let's add a .lintstagedrc in the root of our project, and add the following:

{
  "*.{js,jsx,ts,tsx}": ["yarn lint:fix", "git add"],
  "*.scss": ["prettier --write", "stylelint --fix", "git add"],
  "{*.{json,md}}": ["prettier --write", "git add"]
}

This will run your lint:fix script on commit whilst also running Prettier on *.scss , *.json and *.md files. This will only run on files that are staged.

Next up, add a .huskyrc file to the root of your project and add the following to it:

{
  "hooks": {
    "pre-commit": ["yarn type-check && lint-staged"]
  }
}

This will type-check all your files on the pre-commit hook and run the lint-staged command which in turns runs the commands we've added to the .lintstagedrc file, but only for the staged files.

Now try and commit your new updates... You can't! As the type-check script runs on all your TypeScript files, and all we've done so far is rename *.js files to *ts(x), there are plenty of type and lint errors in there.

If you do want to be able to commit your config files, you can add a --no-verify to your commit command in the terminal.

Wrapping up

So there we have it! You started off with a Gatsby starter that worked perfectly fine and now we've screwed that all up. Your editor is filled with angry squiggly lines and your terminal yells at you when you try to commit your code. Congratulations!

On the bright side, TypeScript, ESLint, Prettier and a bunch of pre-commit hooks are all configured. And that's not all: If you run yarn develop or yarn build, Gatsby will still run. This is because of the Babel configuration I mentioned earlier on. TypeScript errors will not prevent the transpiling of your code as long as the JS is valid.

I do feel kind of guilty leaving you with a blog full of errors, but in the next blog post we'll try to battle the squiggly lines by

  • Installing type definitions for our packages
  • Creating new definitions for packages without their own type definition
  • Making interfaces for objects
  • Trying to generate some definitions for our GraphQL Queries

And whatever else will come on our path.

You can find the progress until so far in the following branch: https://github.com/aderaaij/gatsby-starter-blog/tree/typescript

If you want to see everything up and running without all the errors, you can always have a look at the GitHub repo of Arden.nl

Resources

Discussion

pic
Editor guide
Collapse
nickytonline profile image
Nick Taylor (he/him)

Nice! Here's my repo if you're interested. I was already on Gatsby, but switched to the Netlify CMS starter instead and then basically redesigned most of it and converted the whole codebase to TypeScript.

nickytonline / iamdeveloper.com

Source code for my web site iamdeveloper.com

iamdeveloper.com

Netlify Status

Hey there, I'm Nick and this is my site's source code. This site started off as a clone of the Netlify CMS Gatsby Starter (check it out!). Since then, I've tweaked it a lot and converted the codebase to TypeScript.

Feel free to peruse the code and/or fork it. πŸ˜‰

Thanks to all the wonderful projects that made it possible to build this blog.

To get up and running:

  • clone the repository by running git clone git@github.com:nickytonline/www.iamdeveloper.com.git or git clone https://github.com/nickytonline/www.iamdeveloper.com.git
  • run npm install
  • run npm run develop to get up and running with the Gatsby development server.
  • Since the project uses Babel and not TypeScript as the compiler, a separate process is required to run type checking. Open another terminal and run npm run type-check:watch
  • If you're curious about why the Netlify CMS admin…

I even added Dank Mono to my prismjs setup so code snippets look more sharp.πŸ”₯

I use ESLint as well. Previous projects were always TSLint, but as you mention, tslint is on the way out.

Collapse
ardennl profile image
Arden de Raaij Author

Hey, that's dope! I'll definitely have a look at your site to see what kind of goodness I can steal from there. On that matter I was wondering, have you tried auto-generating type definitions based on GraphQL? I hade a try with graphql-code-generator but barely understood what I was looking at when I saw the output πŸ˜…. I'll have a look at it again when starting on the next post!

Collapse
nickytonline profile image
Nick Taylor (he/him)

I don't auto-generate type definitions based on GraphQL. I'm actually still quite new to GraphQL. My Gatsby site is my only exposure to it so far. I'll check out that project though.

For now I explicitly type my data. See the github.com/nickytonline/iamdevelop... folder.

Thread Thread
ardennl profile image
Arden de Raaij Author

Thanks for your answer. Gatsby was my introduction to GraphQL as well! As I said in my article, it's a gateway-drug :D. I made my own types as well, but I feel that it should be trivial to automate. I just lack some knowledge. Hopefully, I'll have that fixed for the next blog post.

Collapse
antoinerousseau profile image
Antoine Rousseau

Thanks for the post!
I was able to use TypeScript in my Gatsby project without the need of a tsconfig.json, is it really necessary and why?
Also, you can even write your Gatsby Node files in TypeScript too: github.com/gatsbyjs/gatsby/issues/...

Collapse
ardennl profile image
Arden de Raaij Author

Hi Antoine, thanks for the reply and kind words!

As far as I experienced, I did need a tsconfig.json in the root of my project, even if it only contained {}. But this was only to run the tsc command from an NPM script or the command line. If you are not using any type checking through the command line or NPM scripts, you can still benefit from type checking in your editor if it supports it. I guess in that case, you wouldn't explicitly need a tsconfig.json.

Collapse
nickytonline profile image
Nick Taylor (he/him)

You want the tsconfig.json so that you can configure tsc when you run it as just a type checker. At a minimum, you want the compiler option "noEmit" set to true, so it doesn't generate JS files (the default behaviour). We want Babel to do that. Also, for example, in my tsconfig.json, I have it configured for strict mode, github.com/nickytonline/iamdevelop....

Collapse
ooloth profile image
Michael Uloth

Great article!

Are you still planning to write the follow up post you mentioned? I'd love to read it!

Collapse
ardennl profile image
Arden de Raaij Author

Thank you so much Michael! Yes I'm still planning to, I sort of need to get back my writing and experimenting mojo 😬

Collapse
ooloth profile image
Michael Uloth

Tell me about it! Other things tend to get in the way.

Here’s hoping the mojo returns. 🀞

Collapse
fedebabrauskas profile image
Federico Babrauskas

Thanks for the article!

I've made all the process successfully!

I'd love to see your next post about fixing those errors!