Relative TSConfig Projects with `parserOptions.project = true`
"Typed linting", or enabling ESLint rules to tap into the power of the TypeScript type checker, is one of the best parts of typescript-eslint.
But enabling the type checker in repositories with multiple tsconfig.json
files can be annoying to set up.
Even worse, specifying the wrong include paths could result in incorrect rule reports and/or unexpectedly slow lint times.
Improving the setup experience for typed lint rules has been a long-standing goal for typescript-eslint.
One long-standing feature request for that experience has been to support automatically detecting TSConfigs for developers.
We're happy to say that we now support that by setting parserOptions.project
equal to true
in ESLint configurations.
This post will explain what life was like before, what's changed, and what's coming next. ๐
The Problem With Projectsโ
The @typescript-eslint/parser
package is what enables ESLint to parse TypeScript source files.
It converts raw TypeScript code into an "AST" format.
When parserOptions.project
is specified, it additionally sets up TypeScript programs that can be used by typed rules.
Many projects today start with ESLint configs that look something like:
module.exports = {
// ...
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
// ...
};
In larger repos, parserOptions.project
often ends up being one of the three traditionally allowed forms:
- Path, such as
project: './tsconfig.json'
- Glob pattern, such as
project: './packages/**/tsconfig.json'
- Array of paths and/or glob patterns, such as
project: ['./packages/**/tsconfig.json', './separate-package/tsconfig.json']
Explicitly indicating which TSConfig files are used for typed linting can be useful. Developers like being given explicit control over their tooling. However, we've seen a few issues arise from this approach:
- Particularly large repos can end up with so many TSConfig globs, they become confusing to developers or even cause performance issues from overly permissive globs
- Needing to change a template ESLint config every time it's used for a different repository structure is a pain
- Using a TSConfig that's different from what your editor uses can result in different lint reports between the editor and the command-line
Although developers may sometimes need exact control over their parserOptions.project
, most of the time we just want to use the nearest tsconfig.json
to each linted file, which is the TSConfig used by the editor by default.
In other words, many developers want our issue #101: Feature request: support looking up tsconfig.json relative to linted file.
Introducing true
โ
As of typescript-eslint 5.52.0, we now support providing true
for parserOptions.project
:
module.exports = {
// ...
parserOptions: {
project: true,
tsconfigRootDir: __dirname,
},
// ...
};
Doing so indicates that each source file being linted should use type information based on the nearest tsconfig.json
in its directory.
For each file, @typescript-eslint/parser
will check that file's directory, then the parent directory, and so on - until a tsconfig.json
file is found.
We recommend setting the tsconfigRootDir
ESLint config to the project's root directory (most commonly, __dirname
).
That way, if you accidentally delete or rename the root tsconfig.json
file, @typescript-eslint/parser
won't search parent directories for higher tsconfig.json
files.
Why Try true
โ
If your project uses typed linting and manually specifies tsconfig.json
files, we'd highly recommend trying out parserOptions.project: true
.
We've seen it reduce lines of code in ESLint configurations in many early adopters.
Sometimes, it even reduces time spent on typed linting by helping projects use a simpler set of TSConfigs. ๐
In the long term, we're hoping to further improve the configuration and performance for typed linting (see Project Services below). Simplifying your configuration now will make it easier to onboard to our new options when they're available.
How It Worksโ
When @typescript-eslint/parser
is configured to generate type information, it attaches a backing TypeScript "Program" for each file it parses.
Those Programs provide type checking APIs used by lint rules.
Each TSConfig file on disk is generally used to create exactly one Program, and files included by the same TSConfig file will reuse the same Program.
Depending on how the ESLint config's parserOptions.project
was specified, determining which TSConfig file to use for each file can be different:
- For a single path (e.g.
"tsconfig.json"
), only one Program will be created, and all linted files will reuse it. - For globs and/or arrays (e.g.
"./packages/*/tsconfig.json"
), each linted file will use the Program created by the first matched TSConfig file.
For true
, each linted file will first try the tsconfig.json
in its directory, then its parent directory, and so on until one is found on disk or the directory root (parserOptions.tsconfigRootDir
) is reached.
@typescript-eslint/parser
caches those directory tsconfig.json
file lookups for a duration corresponding to parserOptions.cacheLifetime
.
No potential TSConfig path should be checked more than once in a lint run.
See feat(typescript-estree): allow specifying project: true for the backing code changes.
What's Nextโ
Investigating Custom TSConfig Namesโ
Some projects use TSConfig files with names other than tsconfig.json
: most commonly, tsconfig.eslint.json
.
parserOptions.project: true
does not support specifying different name(s) to search for.
We have two followup issues filed to investigate fleshing out that support:
- Enhancement: Allow altering the file names that project: true searches for
- Enhancement: Allow parserOptions.project to be (true | string)[]?
If either of those two issues would benefit you, please ๐ react to them. And if your project has a use case not yet mentioned in their comments, please post that use case. We want to know what's important for users!
Project Servicesโ
The downside of having users specify parserOptions.project
at all is that @typescript-eslint/parser
needs manual logic to create TypeScript Programs and associate them with linted files.
Manual Program creation logic comes with a few issues:
- Complex project setups can be difficult to get right.
- The TypeScript compiler options used in the user's editor might differ from the compiler options in the TSConfigs they specified on disk.
- Files not included in created Programs can't be linted with type information, even though editors still typically surface type information when editing those files.
- Most commonly,
.eslintrc.(c)js
files can be tricky to lint, resulting in the dreaded TSConfig does not include this file error.
- Most commonly,
We're working on an option to instead call the same TypeScript "Project Service" APIs that editors such as VS Code use to create Programs for us instead.
Project Services will automatically detect the TSConfig for each file (like project: true
), and will also allow type information to be computed for JavaScript files without the allowJs
compiler option (unlike project: true
).
We hope this option will eventually become the standard way to enable typed linting.
However, because it's so new and untested, we're keeping it under the EXPERIMENTAL_
prefix for at least all of the 6.X
versions.
See Packages > Parser > EXPERIMENTAL_useProjectService
for more information.
Supporting typescript-eslintโ
If you enjoyed this blog post and/or use typescript-eslint, please consider supporting us on Open Collective. We're a small volunteer team and could use your support to make the ESLint experience on TypeScript great. Thanks! ๐