ts-r-lint

Linter. Replaces ESLint + typescript-eslint.

  • Pre-alpha
  • Works from source

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.

tRPC, same 48 rules, config evaluated each runShorter is faster. Pre-release build, real app.
  • ts-r-lint, config not cached733 ms
  • ESLint 10.10.01,015 ms

1.4× faster linting

What switching looks like

Nothing is on npm yet. Once it is, switching looks like this: ts-r-lint reads the config you already have, so the change is to the command, not the project.

package.json
  "scripts": {
-   "lint": "eslint .",
+   "lint": "ts-r-lint .",
-   "lint:fix": "eslint --fix .",
+   "lint:fix": "ts-r-lint --fix .",
  }

What works, and what doesn’t yet

Works today

  • 103 rules: all 64 of ESLint 10’s recommended set and typescript-eslint’s recommended and strict rules that don’t need types
  • Every case of the published ESLint and typescript-eslint tests for those rules matches
  • ESLint flat config, .oxlintrc.json and biome.json
  • --fix, eslint-disable comments and inline config, with unused-disable reports
  • Output as stylish, json, sarif or GitHub Actions annotations

Not yet

  • No type-aware rules; they are skipped and listed on stderr
  • No ESLint plugins other than typescript-eslint

Measured

Every run, on the same reference app as the home page, with the command and commit that recorded it.

ts-r-lint against ESLint with the same rules

tRPC's own ESLint config with every rule ts-r-lint does not implement removed, given to both tools as the same file, on the same 147 files: 48 rules, without type information on either side. Both report 0 problems from those rules. The bars are the run where ts-r-lint evaluates the config in Node every time, as ESLint does; most of its time is that Node process. By default ts-r-lint caches the evaluated config and then takes 15.5 ms, but ESLint has no such step to skip, so that run is in the table, not drawn next to ESLint. With type-aware rules the runs are not comparable yet: on tRPC ts-r-lint's parity gate turns those rules off (its type check reports one error tsc 7 does not), and with the gate off it reports 11 problems ESLint does not.

tRPC, same 48 rules, config evaluated each runShorter is faster, in ms
  • ts-r
  • Other tools
  • ts-r-lint, config not cached733 ms
  • ESLint 10.10.01,015 ms
Cached config, memory, and the type-aware rules
RunTimePeak memoryNotes
ts-r-lint, same 48 rules, config cached (its default)15.5 ms9 MBSkips evaluating the config, which ESLint does on every run, so not drawn next to ESLint
ts-r-lint, same 48 rules, config not cached733 ms336 MBPeak is the Node process that evaluates the config
ESLint, same 48 rules1,015 ms515 MB0 problems from the rules, like ts-r-lint
ts-r-lint, same 80 rules with the 32 type-aware ones, parity gate off, empty cache952 ms335 MBNot like-for-like: 11 problems ESLint does not report (unbound-method 4, no-unnecessary-type-assertion 4, switch-exhaustiveness-check 2, dot-notation 1). By default the parity gate turns these rules off on tRPC
ESLint, same 80 rules (typescript-eslint projectService)2,605 ms973 MB0 problems from the rules
How we measured
Measured
Wall time of the whole command with hyperfine (cargo xtask bench --repos trpc lint-trpc-same-rules), median of 5 runs after 1 warm-up; peak RSS of the largest process in one extra run. Before each tool's runs, bench/js/eslint-same-rules.mjs writes the restricted configs from the binary's --list-rules. Untyped runs: ts-r-lint --no-type-check, ESLint with parserOptions project and projectService off. Problems compared once from each tool's JSON output by file, rule, line and column
Machine
Apple M-series MacBook (macOS arm64, 16 cores, 48 GB), shared with other jobs while measuring; release build, 4 threads
Software
ts-r-lint release build (crates as of a7210058) with 4 threads; ESLint 10.10.0 and typescript-eslint 8.70.0 from tRPC's lockfile; Node 24.21.0
Input
tRPC packages/server/src and packages/client/src (147 files) at the commit pinned in bench/repos.toml; tRPC's eslint.config.js limited to the 80 of its 126 configured rules ts-r-lint implements (48 without the type-aware ones)
Command
cargo xtask bench --fetch-repos --repos trpc && RAYON_NUM_THREADS=4 cargo xtask bench --repos trpc lint-trpc-same-rules --memory
Recorded in
commit a6bb4b48

ts-r-lint against ESLint and oxlint

Each project linted with its own config. Not like-for-like: ESLint also runs plugin rules ts-r-lint does not have (on TypeORM most of its problems come from jsdoc rules), and where ts-r-check's diagnostics differ from tsc's, ts-r-lint runs without type information (its parity gate), so its type-aware column is a type check plus the untyped rules. Because ESLint does more work, these runs are only in the table, with no bars and no totals. The rules themselves match ESLint's and typescript-eslint's test suites (see Correctness).

Each project with its own config, and memory (not like-for-like)
RunTimePeak memoryNotes
tRPC server and client sources: ts-r-lint / no type check186 / 20 ms148 / 9 MBMedian of 3 runs
tRPC: ESLint, type-aware2,566 ms992 MBAlso runs plugin rules ts-r-lint lacks, so not a like-for-like comparison
TypeORM packages/typeorm/src: ts-r-lint / no type check810 / 194 ms380 / 26 MBMedian of 3 runs
TypeORM: ESLint, type-aware6,718 ms1,748 MBMost of its problems come from jsdoc rules ts-r-lint lacks, so not a like-for-like comparison
Playwright core sources: ts-r-lint / no type check725 / 59 ms465 / 17 MB
Playwright core sources: ESLint5,441 ms2,341 MBAlso runs plugin rules ts-r-lint lacks, so not a like-for-like comparison
date-fns pkgs/core/src: ts-r-lint76 ms14 MB
date-fns pkgs/core/src: oxlint (its own config)348 ms301 MBDifferent rule set, so not a like-for-like comparison
How we measured
Measured
Wall time of the whole command with hyperfine (cargo xtask bench --repos), median of 3 to 5 runs; peak RSS of one extra run
Machine
Apple M-series MacBook (macOS arm64, 16 cores, 48 GB), shared with other jobs while measuring; release build, 4 threads
Software
ts-r-lint release build with 4 threads; each repo's own ESLint or oxlint from its lockfile
Input
tRPC, TypeORM, Playwright and date-fns at the commits pinned in bench/repos.toml, with their own lint configs
Command
cargo xtask bench --fetch-repos --repos trpc,typeorm,playwright,date-fns && cargo xtask bench --repos trpc,typeorm,playwright,date-fns --memory lint-
Recorded in
commit 9c3a371

Correctness

Compared with the tool ts-r-lint replaces, on its own tests and on real code.

  • Lint rules100%

    8,597 of 8,597 cases match

    Every case of the published ESLint and typescript-eslint tests for the 103 rules. Commit 27c280e.

Command line

An excerpt of ts-r-lint --help. You can also run it through the launcher as ts-r lint.

Next to ESLint

A common split: 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 yet. In the editor, ts-r-lsp shows the same diagnostics.

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

  -f, --format <name>      stylish (default), json, sarif, github
  -c, --config <path>      Use this config for every file
      --fix                Apply fixes
      --max-warnings <n>   Exit 1 above this many warnings
      --list-rules         Print the rules ts-r-lint has