@williamthorsen/eslint-config-typescript
v8.5.0
Published
ESLint config for TypeScript, JSON & YML
Readme
@williamthorsen/eslint-config-typescript
Flat-config ESLint preset for TypeScript projects. Covers TypeScript, JavaScript, JSON, YAML, and package.json from a single default export, with opt-in framework configs for React, Next.js, JSX A11y, Vitest, and React Testing Library.
Release notes — v8.5.0 (2026-08-05)
🎉 Features
Disable conflicting class-member-order rule
Disables
unicorn/consistent-class-member-orderbecause it conflicts with a similar@typescript-eslintrule.
Installation
pnpm add -D @williamthorsen/eslint-config-typescript eslint typescriptRequires ESLint 9+ and TypeScript 5+.
Quick start
// eslint.config.ts
import { defineConfig } from 'eslint/config';
import tsConfig from '@williamthorsen/eslint-config-typescript';
export default defineConfig(tsConfig, {
languageOptions: {
parserOptions: {
// Anchor type-aware linting at your repo root.
tsconfigRootDir: import.meta.dirname,
},
},
// your overrides
});Everything this package exports is typed with ESLint core's own Config, so the same composition typechecks unchanged in an eslint.config.ts — no tseslint.config(), type assertion, or widening cast.
Type-aware linting
The TypeScript rules are type-aware, and the preset enables typescript-eslint's project service (parserOptions.projectService), so each file's owning tsconfig.json is discovered automatically — you do not set parserOptions.project. Two requirements follow:
- Every linted
.ts/.tsxfile must belong to a discoverabletsconfig.jsonthrough itsinclude. A file outside every project — for example a test directory excluded from your build config — must be added to sometsconfig.json'sinclude, or ESLint reports it as not found in any project. - Set
tsconfigRootDir(as in Quick start) to anchor resolution at your repo root. Without it, resolution falls back to the current working directory, which varies by how ESLint is launched.
Migrating from parserOptions.project
This section covers the parser change alone. For the complete v5 → v6 upgrade — the Node and ESLint floors, the package bump, and post-upgrade cleanup — see Migrating to v6.
Earlier versions left type-information wiring to the consumer: you set parserOptions.project and usually kept a dedicated tsconfig.eslint.json. This version supplies projectService itself, so:
- Remove
parserOptions.projectfrom your ESLint config. Leaving it set now throwsEnabling "project" does nothing when "projectService" is enabled. - Fold any lint-only
tsconfig.eslint.jsonincludeentries into the realtsconfig.json, then delete thetsconfig.eslint.json. Wideningincludeis safe when the config is typecheck-only. - Keep only
tsconfigRootDir: import.meta.dirnamein yourparserOptions.
What's included
The default export bundles configs for the following surfaces:
| Surface | File pattern | Notable plugins |
| -------------- | ------------------------------ | ----------------------------------------------------------------- |
| TypeScript | **/*.{ts,cts,mts,tsx} | typescript-eslint (type-aware), sky-pilot |
| JavaScript | **/*.{js,cjs,mjs,jsx} | core rules, JS-specific conventions |
| Cross-cutting | all code files | eslint-comments, import, n, simple-import-sort, unicorn |
| JSON / JSON5 | **/*.{json,json5} | jsonc |
| YAML | **/*.{yaml,yml} | yml |
| package.json | **/package.json | package-json (recommended + stylistic) |
| Tests | **/*.{spec,test}.{js,ts,...} | strict TypeScript rules relaxed |
Test files (*.spec.* / *.test.*) have several strict rules disabled (e.g., no-unsafe-assignment, unbound-method, no-extraneous-class) so spec files don't fight the type checker. Declaration files (*.d.ts) have import/no-duplicates turned off.
Granular config access
The default export is the catch-all "everything on" preset. For à la carte composition, import individual configs:
import { defineConfig } from 'eslint/config';
import { configs } from '@williamthorsen/eslint-config-typescript';
export default defineConfig(
configs.typeScript,
configs.import,
// skip configs.unicorn entirely
);| Config | Targets |
| -------------------------- | ------------------------------------------ |
| configs.javaScript | core JavaScript rules |
| configs.typeScript | TypeScript rules (type-aware) |
| configs.eslintComments | eslint-disable comment hygiene |
| configs.import | import order and resolution |
| configs.n | Node.js (eslint-plugin-n) rules |
| configs.simpleImportSort | sorted imports |
| configs.unicorn | eslint-plugin-unicorn rules |
| configs.json | JSON rules |
| configs.json5 | JSON5 rules — apply after configs.json |
| configs.packageJson | package.json rules |
| configs.yaml | YAML rules |
Framework configs (lazy-loaded)
Framework-specific configs are exposed via createConfig so their plugin dependencies (eslint-plugin-react, @next/eslint-plugin-next, etc.) load only when used. Every factory resolves to a config array, so spread each one (or pass them through extends):
import { defineConfig } from 'eslint/config';
import config, { createConfig } from '@williamthorsen/eslint-config-typescript';
export default defineConfig(
config,
...(await createConfig.react()),
...(await createConfig.jsxA11y()),
...(await createConfig.next()),
);Scope the test-oriented factories to your test files rather than spreading them across the whole project. Applied to ordinary source, vitest/require-hook reports on every top-level statement:
import { defineConfig } from 'eslint/config';
import config, { createConfig, patterns } from '@williamthorsen/eslint-config-typescript';
export default defineConfig(config, {
files: patterns.testFiles,
extends: [await createConfig.vitest(), await createConfig.reactTestingLibrary()],
});patterns.testFiles covers JavaScript as well as TypeScript test files. Three of the Vitest rules read type information — unbound-method, valid-title, and prefer-describe-function-title — and each aborts the ESLint run rather than degrading when a file has no parser services, so createConfig.vitest() disables all three on JavaScript globs. That makes the scoping above safe whether or not your JavaScript test files get a type-aware parser.
| Method | Loads |
| ------------------------------------ | -------------------------------------------------- |
| createConfig.react() | eslint-plugin-react, eslint-plugin-react-hooks |
| createConfig.next() | @next/eslint-plugin-next |
| createConfig.jsxA11y() | eslint-plugin-jsx-a11y |
| createConfig.reactTestingLibrary() | eslint-plugin-testing-library |
| createConfig.vitest() | @vitest/eslint-plugin |
These plugins are declared as devDependencies of this package. Install them yourself in projects that use them.
createConfig.react() pins settings.react.version to a recent default, because eslint-plugin-react's 'detect' mode is incompatible with ESLint 10 (it calls a removed API). Override it to match your React version by appending a settings block:
export default defineConfig(config, ...(await createConfig.react()), {
settings: { react: { version: '18.3' } },
});File patterns
For composing your own scoped configs without re-deriving the globs:
import { defineConfig } from 'eslint/config';
import { patterns } from '@williamthorsen/eslint-config-typescript';
export default defineConfig({
files: patterns.typeScriptFiles,
rules: {
// TypeScript-only overrides
},
});| Constant | Value |
| ------------------------------- | ---------------------------------------------------------------------------- |
| patterns.javaScriptFiles | ['**/*.{js,cjs,mjs,jsx}'] |
| patterns.typeScriptFiles | ['**/*.{ts,cts,mts,tsx}'] |
| patterns.codeFiles | both of the above |
| patterns.testFiles | ['**/*.{spec,test}.{js,cjs,mjs,jsx}', '**/*.{spec,test}.{ts,cts,mts,tsx}'] |
| patterns.javaScriptExtensions | ['{js,cjs,mjs,jsx}'] |
| patterns.typeScriptExtensions | ['{ts,cts,mts,tsx}'] |
| patterns.codeExtensions | both |
Advisory rule severities
advisoryRuleSeverities maps the rules this config sets to 'warn' because they report style and modernization advice rather than defects — @typescript-eslint/no-deprecated, most of the unicorn prefer-* set, and their neighbours. Rules this config disables outright are not included.
Use it with @williamthorsen/strict-lint to exempt them from error promotion, so a stricter CI run still fails on genuine defects only:
// .config/strict-lint.config.ts
import { advisoryRuleSeverities } from '@williamthorsen/eslint-config-typescript';
import { defineConfig } from '@williamthorsen/strict-lint/config';
export default defineConfig({
maxSeverity: { ...advisoryRuleSeverities },
});Or spread it into an ordinary flat-config rules block to set those severities directly:
import { advisoryRuleSeverities } from '@williamthorsen/eslint-config-typescript';
export default [{ rules: { ...advisoryRuleSeverities } }];An unscoped block applies 'warn' everywhere, including in test files, where this config turns unicorn/consistent-function-scoping and unicorn/no-useless-undefined off. Scope the block with files to keep those exceptions.
Peer dependencies
| Dependency | Required |
| ------------ | -------- |
| eslint | >=10 |
| typescript | >=5 |
License
ISC.
