Skip to main content

Relative TSConfig Projects with `parserOptions.project = true`

ยท 6 min read
Josh Goldberg
typescript-eslint Maintainer

"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.

tip

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.

note

@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:

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.

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! ๐Ÿ’–