TypeScript

Fix 'was not found by the project service' in ESLint

Stop ESLint from type-checking your compiled dist output, and avoid the newer project-vs-projectService conflict that now hides this exact message.

Mahdi Ben RhoumaMahdi Ben Rhouma
9 min read

A Stack Overflow question with 22,143 views lays out a project with a src/ folder compiled to dist/ by tsc, and an eslint.config.mjs that already lists dist/** under ignores. ESLint still fails on the compiled output:

text
/Users/kainzhong/VSCode/eslintTmp/dist/src/index.d.ts
  0:0  error  Parsing error: /Users/kainzhong/VSCode/eslintTmp/dist/src/index.d.ts was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject

TL;DR#

The config's ignores sits in the same object as files and rules, which makes it a scoped, non-global ignore: it switches off that block's own two rules for dist/**, but never removes those files from linting, so the type-aware presets spread in further down the array still hand them to typescript-eslint's projectService — which then refuses files its nearest tsconfig.json never listed. The fix is to give ignores its own top-level config object. This config also still sets parserOptions.project next to projectService; drop project too, because current typescript-eslint treats keeping both as its own error, and that is the message which actually surfaces first today.

Why it happens#

Two separate mechanisms stack here, and the question's config trips both.

ESLint's ignores has two jobs, and which one it does depends on what else shares its config object. Per ESLint's configuration docs: when ignores is used without any other keys (besides name) in the configuration object, the patterns act as global ignores and apply to every configuration object in the array; when ignores is used alongside other properties such as files or rules, the patterns act as non-global ignores and apply only to the configuration object in which they are defined. The question's first config object has files, ignores and rules together, so its ignores is non-global — it only stops @typescript-eslint/no-unnecessary-condition and @typescript-eslint/no-unnecessary-type-assertion from applying to dist/**. ESLint's own default file match (**/*.js, **/*.cjs, **/*.mjs) and the ...tseslint.configs.strictTypeChecked / ...tseslint.configs.recommendedTypeChecked spreads further down the array are untouched by it, so they still pick up dist/src/index.d.ts and the other compiled files.

typescript-eslint's project service is the second half. typescript-eslint 8 stabilised parserOptions.projectService, which calls the same TypeScript "project service" APIs that editors such as VS Code use, so it can automatically find the nearest tsconfig.json for each linted file instead of a hand-maintained list. For every file it is handed, it looks for that nearest tsconfig.json and tries to build a TypeScript program for it; the question's tsconfig.json only includes src/**/*.ts (dist/** is in its own exclude), so the moment the project service is handed a file under dist/, there is no program to put it in, and typescript-eslint's own troubleshooting guide confirms this is exactly that failure mode — "attempting to use the project service to lint a file not explicitly included in its nearest tsconfig.json". outDir is also why the path is dist/src/index.d.ts and not dist/index.d.ts: with rootDir set to the project root rather than src, TypeScript's own outDir reference says the directory structure of the original source files is preserved under outDir, so src/index.ts lands at dist/src/index.js.

Fix#

1. Give ignores its own config object, with no files or rules next to it, so it is global:

js
// Before — ignores shares a block with files and rules: scoped, not global.
export default tseslint.config(
  {
    files: ['src/**/*.ts'],
    ignores: ['dist/**/*.ts', 'dist/**', '**/*.mjs', 'eslint.config.mjs', '**/*.js'],
    rules: {
      '@typescript-eslint/no-unnecessary-condition': 'error',
      '@typescript-eslint/no-unnecessary-type-assertion': 'error',
    },
  },
  ...tseslint.configs.strictTypeChecked,
  ...tseslint.configs.recommendedTypeChecked,
  // ...
);
js
// After — ignores is alone in its own block: global, so every config
// object below it (including the two spreads) skips these paths too.
export default tseslint.config(
  {
    ignores: ['dist/**/*.ts', 'dist/**', '**/*.mjs', 'eslint.config.mjs', '**/*.js'],
  },
  {
    files: ['src/**/*.ts'],
    rules: {
      '@typescript-eslint/no-unnecessary-condition': 'error',
      '@typescript-eslint/no-unnecessary-type-assertion': 'error',
    },
  },
  ...tseslint.configs.strictTypeChecked,
  ...tseslint.configs.recommendedTypeChecked,
  // ...
);

Reproduced on Node 22.23.3 (official node:22-alpine image), with typescript-eslint 8.72.0, ESLint 10.13.0 and TypeScript 6.0.3 freshly installed, 25 October 2026: with only this change applied and parserOptions otherwise untouched, the four dist/ files drop out of the report entirely — only the two real source files remain, still failing, which is the second half of the fix below.

2. Drop parserOptions.project if projectService is also set. The question's languageOptions.parserOptions sets both:

js
export default tseslint.config({
  languageOptions: {
    parserOptions: {
      project: './tsconfig.json',
      projectService: true,
      tsconfigRootDir: __dirname,
    },
  },
});

Reproducing the question's eslint.config.mjs (with ignores already moved per step 1, and baseUrl removed from its tsconfig.json for the reason in the section below), every remaining linted file fails with a different message, not the one in this article's title:

text
  0:0  error  Parsing error: Enabling "project" does nothing when "projectService" is enabled. You can remove the "project" setting

typescript-eslint's parser docs confirm this is deliberate: "Enabling both project and projectService at the same time will cause an error." It fires before the project-service file-matching error ever gets a chance to, on every linted file, config file included — so a project still shaped exactly like the question's reproduces this message first, not the "was not found by the project service" one it asked about. Removing only project and leaving the ignores bug in place confirms the reverse: the project-service message comes straight back, on the dist/ files and, since eslint.config.mjs is itself outside the tsconfig.json's include, on that file too:

text
  0:0  error  Parsing error: /work/eslint.config.mjs was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject

Remove the project line once projectService is set:

js
export default tseslint.config({
  languageOptions: {
    parserOptions: {
      projectService: true,
      tsconfigRootDir: __dirname,
    },
  },
});

3. Optional: make the global/scoped split impossible to get wrong again. ESLint ships a globalIgnores() helper for exactly this ambiguity:

js
import { defineConfig, globalIgnores } from 'eslint/config';
 
export default defineConfig([
  globalIgnores(['dist/**/*.ts', 'dist/**', '**/*.mjs', 'eslint.config.mjs', '**/*.js']),
  {
    files: ['src/**/*.ts'],
    rules: {
      '@typescript-eslint/no-unnecessary-condition': 'error',
      '@typescript-eslint/no-unnecessary-type-assertion': 'error',
    },
  },
  // ...
]);

Verify the fix#

With both changes in place — ignores in its own block, project removed — the same reproduction run ends with:

text
eslint exit code: 0

No files printed, nothing to fix: the two source files lint cleanly with type information, and the compiled dist/ output, the config file itself, and anything else outside src/**/*.ts are excluded from every config object, not just the one that happened to declare ignores first.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

What a reader of this fix hits next#

If the files should actually be linted, not ignored. When the goal is type-aware linting on a handful of files the nearest tsconfig.json genuinely does not cover — a .js script, the ESLint config itself — add them to that tsconfig.json's include — and take them back out of its exclude if they are listed there too, because exclude otherwise keeps winning: TypeScript's own reference says it "only changes which files are included as a result of the include setting", so a file named in both stays out. If that is not possible, typescript-eslint's parser docs describe projectService.allowDefaultProject: a glob list of "out of project" files linted against a default, limited program instead. It is capped at 8 files by default, cannot use **, and a file cannot appear in both allowDefaultProject and its nearest tsconfig.json's include at once — it is meant for a handful of stragglers, not a replacement for a real tsconfig.json entry, because every file it covers rebuilds its own throwaway TypeScript program. tsconfig.json has other automatic-inclusion surprises too, entirely independent of ESLint — see Fix Cannot find type definition file for 'body-parser' for a case where a single broken folder under node_modules/@types used to fail an entire project before TypeScript 6.0.

If you copied this exact tsconfig.json. It also sets "baseUrl": "./src". Running tsc -p tsconfig.json to produce the dist/ folder in the first place now fails immediately on TypeScript 6.0, before eslint is even invoked:

text
tsconfig.json(12,9): error TS5101: Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.

Reproduced the same way, on TypeScript 6.0.3. baseUrl on its own changes how bare, non-relative imports resolve, taking priority over node_modules lookups — TypeScript's own reference is explicit about that — so delete it only if nothing in the project actually imports through it; this reproduction's two files use relative imports and lose nothing by dropping it. Otherwise, add "ignoreDeprecations": "6.0" instead, exactly as the error says — a stopgap, not a fix: the same message warns that baseUrl stops working completely once a project moves to TypeScript 7.0.

If the wording is different. "ESLint was configured to run on <file> using parserOptions.project: … However, that TSConfig does not include this file" is the older parserOptions.project message rather than this projectService one — Fix 'ESLint was configured to run on' in Cypress tests walks through that version of the same underlying problem, including why switching to projectService is one of its own fixes. Not every parsing error on a declaration-shaped path comes from ESLint either: tsc raises its own, like TS1084, when an old TypeScript release cannot read a newer @types/node — a compiler-side failure with nothing to do with projectService.

Once the build itself is clean. tsc emits dist/src/index.js next to dist/src/index.d.ts; a project that also uses path aliases in tsconfig.json can hit a second, unrelated error the moment that compiled file is run directly with node — Fix Cannot find module defined in tsconfig paths at runtime covers the fixes that were actually tested against that one. None of these four replace the fix above — they are simply what else sits along the same path for this exact question's project.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote