little cubes

TypeScript config files with type-aware ESLint

How to keep *.config.ts files in TypeScript without breaking your lint setup

I want three things simultaneously:

  1. Config files written in typescript (e.g. vite.config.ts)
  2. Those config files to be excluded from typescript compilation
    • I don’t ever want them compiled into JS
    • I also don’t want my build to fail because of a typescript error in my config file
  3. I want to have type-aware eslint rules

typescript-eslint enables type-aware lint rules. To do that it needs to load a TypeScript project, and it finds that project by looking for a tsconfig.json that includes the file being linted.

eslint.config.ts
import {defineConfig} from 'eslint/config'
export default defineConfig([
{
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
projectService: true,
},
},
},
])

This creates a damned-if-you-do, damned-if-you-don’t situation with config files written in typescript (*.config.ts files).

  • If you include them in tsconfig.json: TypeScript tries to compile them as part of your app.

  • If you exclude them from tsconfig.json: eslint throws an error because typescript-eslint can’t find a a tsconfig.json for them:

    error Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
    The file was not found in any of the provided project(s): vite.config.ts
  1. Exclude config files from compilation:

    tsconfig.json
    {
    "exclude": ["*.config.ts"]
    // Optional: Prevent your editor from showing ts errors in a .test.js file, for example
    // "include": ["/**/*.ts", "/**/*.tsx"]
    }
  2. Tell ESLint to just use a default tsconfig.json to when assessing type-aware rules for your config files

    eslint.config.ts
    {
    files: ['**/*.ts', '**/*.tsx'], // Optional: only enable type-aware linting for ts files
    languageOptions: {
    parserOptions: {
    tsconfigRootDir: import.meta.dirname,
    projectService: true,
    projectService: {
    allowDefaultProject: ['*.config.ts'],
    },
    },
    },
    }

allowDefaultProject tells typescript-eslint: for files matching this glob, create a synthetic TypeScript project on the fly using default compiler options, instead of requiring them to be in a real tsconfig.json.

The files get type-aware linting. They just don’t go through your actual tsc compilation target.

The tsconfig.json "exclude" is still needed. Without it, TypeScript would try to include vite.config.ts in your app’s type program when it compiles. Config files don’t belong there — they pull in Node types, Vite-specific globals, and other things that don’t belong in your app bundle.

So the two changes work together:

  • tsconfig.json exclusion: “don’t compile this”
  • allowDefaultProject: “but do lint this with types”