• Alpha
  • On npm: @ts-r.dev/lint

ts-r-lint: linter

A linter with 103 ESLint and typescript-eslint rules that reads eslint.config, .oxlintrc.json or biome.json. No type-aware rules yet.

ts-r-lint runs ESLint-compatible rules over JavaScript and TypeScript. It reads the config you already have, reports in ESLint's formats and exits with ESLint's codes, so it can stand in for eslint in scripts and CI when your config only uses the rules it has.

What works today

  • 103 rules: all 64 rules of ESLint 10's recommended set, the typescript-eslint 8.71 recommended and strict rules that don't need type information, and a few more. ts-r-lint --list-rules prints them.
  • Every case of the published ESLint and typescript-eslint tests for those rules matches: 8,597 of 8,597.
  • Configs: ESLint flat config, .oxlintrc.json and biome.json (see below).
  • --fix for rules with fixes.
  • eslint-disable comments and /* eslint */ inline config, with reports of unused disable comments.
  • .gitignore is honoured unless you pass --no-gitignore.
  • Output formats stylish, json, sarif and github (GitHub Actions annotations).

Not yet

  • No type-aware rules. Rules that need type information are skipped and listed on stderr as "needs type information".
  • No ESLint plugins other than typescript-eslint. Rules from other plugins are skipped and listed as "plugin not implemented".

Design record: 0006 lint.

Command line

ts-r-lint [options] [paths...]

Lints JavaScript and TypeScript files with ESLint-compatible rules and configs
(eslint.config.*, .oxlintrc.json, biome.json).

Options:
  --fix                         Apply fixes and write files
  -f, --format <name>           stylish (default), json, sarif, github
  -c, --config <path>           Use this config for every file
  --max-warnings <n>            Exit 1 when there are more than n warnings
  --quiet                       Report errors only (warn rules are not run)
  -o, --output-file <path>      Write the report to a file
  --threads <n>                 Worker threads (default: min(4, cores))
  --rule <name>                 Only run this rule (repeatable)
  --no-gitignore                Do not honour .gitignore files
  --no-warn-ignored             Do not warn about ignored files named explicitly
  --no-inline-config            Ignore eslint-disable and /* eslint */ comments
  --report-unused-disable-directives
                                Report unused eslint-disable comments as errors
  --report-unused-disable-directives-severity <sev>
                                off, warn or error
  --list-rules                  Print the built-in rules and exit
  -h, --help                    Show this help
  -v, --version                 Show the version
  • --quiet doesn't run warn rules at all, rather than running them and hiding the result.
  • --rule narrows a run to one or more rules, which helps when you compare against ESLint.
  • Only the report goes to stdout (or to --output-file), so -f json can be piped. Notes go to stderr.
  • ESLint doesn't read .gitignore. Use --no-gitignore when you compare the two.

Exit codes match ESLint's: 0 when there are no errors (and warnings are within --max-warnings), 1 when there are errors or too many warnings, 2 for a config or usage error.

You can also run it through the launcher as ts-r lint.

Config

For each file, ts-r-lint looks at or above the file's directory, in this order:

OrderConfigHow it is read
1Nearest eslint.config.{js,mjs,cjs,ts,mts,cts}Evaluated by Node (all configs in one process), then cached in the lint-config namespace of ~/.cache/ts-r
2Nearest .oxlintrc.jsonRead directly
3Nearest biome.json or biome.jsoncRead directly; Biome rules with an ESLint equivalent
4None foundESLint recommended, plus typescript-eslint recommended for TypeScript files

Flat configs merge per file with ESLint 10's rules. The config cache is invalidated when the config, anything it imports locally or the nearest lockfile changes. --config uses one config for every file instead, with patterns relative to the current directory.

Rules a config turns on that ts-r-lint doesn't have are listed once per config on stderr, with the reason: needs type information, plugin not implemented, not implemented yet, or unknown.

Environment variables:

VariableEffect
TS_R_NODENode binary used to evaluate eslint.config.* (default: node on PATH)
TS_R_LINT_NODE_WRAPPERCommand to wrap that Node process with
TS_R_LINT_NO_CONFIG_CACHE=1Evaluate the config every run (for cold timings)

Next to ESLint

ts-r-lint works on its own. A common split is to run ts-r-lint on every save or commit for the fast rules, and keep ESLint in CI for type-aware rules and plugins ts-r-lint doesn't have. In the editor, ts-r-lsp can show the same diagnostics.

Measured

Like-for-like, on tRPC's server and client sources (147 files): tRPC's own ESLint config with every rule ts-r-lint doesn't implement removed, the same file for both tools (cargo xtask bench --repos trpc lint-trpc-same-rules, median of 5 runs):

Runts-r-lintESLint 10.10.0
Same 48 rules, no type information, config evaluated by Node every run733 ms1,015 ms
Same, with ts-r-lint's config cache (its default)15.5 ms1,015 ms

Both report 0 problems from those rules. Most of ts-r-lint's uncached time is Node evaluating eslint.config.js. With the 32 type-aware rules as well the runs aren't comparable yet: on tRPC the parity gate turns them off, and with the gate off ts-r-lint reports 11 problems ESLint doesn't.

Apple M-series Mac (a shared laptop), 4 threads, each project's own lint config, on public projects pinned in bench/repos.toml (cargo xtask bench --repos, median of 3 to 5 runs):

Projectts-r-lintts-r-lint, no type checkProject's linter
tRPC server and client sources186 ms20 msESLint (type-aware): 2,566 ms
TypeORM packages/typeorm/src810 ms194 msESLint (type-aware): 6,718 ms
Playwright core sources725 ms59 msESLint: 5,441 ms
date-fns pkgs/core/src76 msoxlint: 348 ms

These aren't like-for-like yet. ESLint runs plugin rules ts-r-lint doesn't have (on TypeORM, most of ESLint's problems come from jsdoc/* rules), and when ts-r-check's diagnostics differ from tsc's on a project, ts-r-lint runs its rules without type information (the parity gate), so the first column measures a type check plus the untyped rules. The type-aware rules themselves match ESLint's test suites (see Correctness).

Method and commits: same rules, lint and correctness.