Typescript Linting & custom flat config
On this page we will start by using typescript-eslint to be able to lint typescript files, then we will create a custom next configuration, which will allow us to get rid of any compatibility mode (FlatCompat) and we will fine tune what sets of rules we want to include (or not)
typescript-eslint installation and configuration
When using CNA earlier in this tutorial, to set up our Next.js 15 project, we told it that we wanted to enable Typescript, which means our current configuration uses next/typescript, which (as the documentation explains well) adds the recommended rules from the @typescript-eslint/eslint-plugin
There is one major difference between classic configs and flat config setups for typescript-eslint, for an ESLint v8 setup you would usually install two packages, @typescript-eslint/eslint-plugin the package to the tseslint plugin and also the @typescript-eslint/parser parser package, but since ESLint 9 most tutorials (that I saw) had switched to using a single typescript-eslint package that contains the plugin, the parser as well as configurations
We start by installing typescript-eslint package using the following command:
Next we will fine tune our typescript linting by adding a custom/typescript-eslint/recommended config:
Line 3: we import typescript-eslint (which we installed in the previous step)
Line 4: we import the FlatConfig type from the @typescript-eslint utilities (which we are going to use instead of the type from the ESLint package)
Line 29: we use the config()
function from the typescript-eslint package to create a typescript eslint config
Line 31: first we specify that the name for custom flat config will be tseslintConfig
Line 32: We use the files
option to tell ESLint which files (extensions) we want to include (for this tutorial we will enable linting for ES Module (esm) files (.mjs), for regular Typescript (.ts) and also for Typescript & JSX files (.tsx))
Lines 33 to 45: we use the extends option to add full sets of rules to our setup (you can still fine tune rules from those sets using the rules config option):
When using typescript-eslint you get several different tseslint.configs.*
you can chose from, the recommended is config contains a set of recommended rules (this is what Next.js uses), there is also recommendedTypeChecked which adds recommended rules that require type information; Next we have strict and StrictTypeChecked, which you could use instead of the recommended rules, these sets include rules are more opinionated (which is why some projects don't use them, however they can help catch bugs so I recommend giving it a try, you can still disable them later by switching back to recommended rules or you could disable some of the rules by setting them to off)
Then we have to chose between two more sets of rules, the stylistic set contains rules that do NOT impact program logic but focus on enforcing "simpler code patterns", which means they are even more , and then there is stylisticTypeChecked the second option (which I will use in this tutorial) which adds a few more stylistic rules that require type information
To better understand what is in those sets I highly recommend checking out the typescript-eslint rules overview page
Lines 47 to 59: Then we add some parser options to configure the typescript-eslint parser; the projectService got introduced in typescript-eslint v8, as they explain in their performance documentation it is recommended to use the default projectService when ever possible, they also point out that adding the path to your tsconfig.json manually "cause more disk IO", which would result in a slower linting process (issue #2611); those options are only needed if you use typed linting, so when you use one of the "type checked" sets (if are NOT using any TypeChecked
rules, then you can comment out or delete the languageOptions
block)
the ecmaFeatures parser option in the languageOptions is commented out because there are other configurations (that we will add to our setup in an upcoming chapter) that set these options for us
I keep the comment for now as reminder, but will delete it when we add the eslint-plugin-react package, as this package will already set the option
Line 58: It is recommended to keep the typescript-eslint warnOnUnsupportedTypeScriptVersion option enabled (which is why I explicitly added it to the config), if the Typescript version that you have installed is higher than the version typescript-eslint supports then it will show a warning message (when launching the linting process)
Lines 65 to 67: we create yet another config to tell typescript eslint to apply the disableTypeChecked option when parsing ES Module (esm) files (.mjs)
For this config we add the custom/typescript-eslint/disable-type-checked name at the end, this is because the order matters; if we would have set the name first and then added the disableTypeChecked config, then our name would get replaced by the name that the config sets; but because we add our name last it will replace the name that got set by the disableTypeChecked config
Line 72: we can now remove next/typescript
from the compatibility mode (compat.extends), as it gets replaced by our custom typescript-eslint config
Line 76: we set the FlatConfig type from the @typescript-eslint utilities as type for our default export (to replace the type from the ESLint package which is what we were using previously), the reason to switch for the Config type from ESLint to the one from @typescript-eslint utilities is because of issues related with the type of the default export, you can read more about those issues in the next chapter
Why we switched from ESLint to typescript-eslint types
In the first ESLint chapters, we used the type from the ESLint package like this:
(...)
However if do keep using those types and want to use typescript-eslint then you do will start to see typescript errors like this one:
Type
(Config | FlatConfig)[]
does not satisfy the expected typeConfig<RulesRecord>[]
.
There are several related tickets about this, like for example issue #8613 and issue #9724, the TLDR is use the types from typescript-eslint
This is why in the latest example, we switched to use the type from the typescript-eslint package instead:
(...)
Replacing eslint-config-next with a custom flat config
Before we can start adding linting for MDX files and markdown content we first need to get rid of eslint-config-next
I usually recommend to only remove component(s) or disable a feature(s) of a framework if it is really necessary, as doing so will increase your maintenance costs and your technical debt counter will go up, there are however situations where it is ok to make the transition
In the future eslint-config-next will hopefully get converted to a real ESLint v9 sharable flat config and then this chapter will be obsolete
As eslint-config-next needs the FlatCompat, we are going to create a custom version that does not need a compat mode. This is more work but it has the benefit that we will be able to adjust what sets of rules we want to use, meaning instead of only using the recommend rules of each plugin we can add stricter or more opinionated sets of rules (that are not enabled by default)
In this tutorial we will use the eslint-plugin-import package (as does eslint-config-next), there is also eslint-plugin-import-x, which is another popular choice you might want to try out
We will also NOT use @rushstack/eslint-patch (which is now ESLint v9 compatible), however if you use Next.js in a big monorepo then you should probably install / configure it too
For current situation of the eslint-plugin-react-hooks plugin is similar to the situation of the Next.js eslint plugin, the PR (#28773) to add "ESLint v9 support" to the react-hooks plugin got merged, however the ticket about adding support for flat config is not closed (yet), there is however a PR (#30774) in the works
The plugins we will use are eslint-plugin-import (and if you use typescript also eslint-import-resolver-typescript), eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-jsx-a11y, and we install them using the following command:
As we are attempting to reproduce what eslint-config-next, this means that we are installing dependencies that we already in the project, but it will make sure that we are not missing a package in our stack and it will ensure we have the latest version installed for each of them (as we added the @latest tag in the command(s))
If you are using Typescript, you will also want to install the following types:
Unfortunately several packages (eslint-plugin-react-hooks, eslint-plugin-import and @next/eslint-plugin-next) have no types, but I assume this will change as more users transition to .ts config files
Now that we have our packages installed we can finally start updating our eslint.config.ts again:
Lines 4 to 11: we import the 5 plugins, but most importantly we get rid of the FlatCompat import (from '@eslint/eslintrc') as we will NOT need it anymore 🎉 (more details bout this change are in the next chapter "Removing ESLintRC support")
If our configuration is a typescript file, then you have to use a typescript comment to inform typescript that it will not find the types for some of the packages, as NOT all packages have types (yet?)
Line 72: we create a custom config to replace configuration from eslint-config-next package
Line 74: we set the name of our config to custom/next/config
Lines 76 to 84: we add the 5 plugins to the config (we use an eslint disable comment for the 3 plugins that don't have types)
Lines 85 to 110: we use rules option to add entire sets of rules to our config but then also to fine tune some of the rules (from those sets):
- we first add the react (eslint plugin) recommended rules set
- more details about each rules can be found in their eslint-plugin-react readme)
- next we add the react jsx-runtime rules (since React 17 the new JSX transform will automatically import the necessary "jsx-runtime functions" rules (for example rules that were checking if React was in scope when using JSX are not needed anymore)
- the third set we are add are the recommended rules from the react-hooks plugin
- then we add the @next/next (Next.js ESLint) plugin rules, we add the recommend as well as the strict rules (which also get called "core web vitals rules" as it contains rules that will help you improve your core web vitals by using tools that are built in Next.js)
- then we add the recommend rules set from the import plugin
- if you use typescript, then there the import plugin also has an additional set of typescript rules, which we are however NOT using due to a problem with some imports (as described in the eslint-plugin-import issue #2969)); Instead we earlier added the eslint-import-resolver-typescript package to handle imports in typescript files properly (and later we will add some options to the settings to make this work)
- more details about each rules can be found in their eslint-plugin-import readme
- next we add the recommend eslint-plugin-jsx-a11y rules, this plugin offers two sets of rules the recommended and the strict, we will use the strict version (feel free to use recommended instead if you prefer less strict rules or disable some of the more strict rules manually)
- more details about each rules can be found in their eslint-plugin-jsx-a11y readme
- typescript users, note that we also use the FlatConfig.Rules type to
Lines 111 to 121: next we have the use the settings option to pass some options to the plugins we are using, the first setting is for react plugin and tells the plugin to automatically detect what react version we are using (this often works well and avoids having to update a version manually), then we set the alwaysTryTypes setting from our import resolver to true (there are more options you could set, if for example you use a monorepo), to explain what this does I will quote the explanation from their readme:
always try to resolve types under
<root>@types
directory even it doesn't contain any source code, like@types/unist
Congratulations 🎉 you just added a custom next config to your linting setup, which means your npm run lint
command will now also detect problems related to react, hooks, imports, next.js and accessibility
Removing ESLintRC support
At first we had two plugins that needed the FlatCompat import from @eslint/eslintrc package. In the "Custom ESLint 9 flat config for Next.js 15" chapter (of the previous page) we first converted our classic ESLintRC config to a flat config and then later in the Typescript Linting chapter (of the current page) we added our own Typescript-eslint configuration. Due to those changes we were then able to get rid of the next/typescript plugin (the first of two plugins still depending on the compatibility mode). Then in the previous chapter we have added a custom nextConfig, which allowed us to also remove next/core-web-vitals.
Now that we got rid of both plugins that required a compatibility mode we can finally also remove the ESLintRC Library package (as well as the types package if like me you use typescript) from our dependencies, using the following command:
Congratulations 🎉 if you made it this far, we had to make a lot of changes, but you now know a lot more about how ESLint and typescript-eslint work, and we are now ready to start adding linting for MDX content in the next part of this tutorial
If you liked this post, please consider buying me a coffee ☕ or sponsor ❤️ me on GitHub, as it will help me create more content and keep it free for everyone