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.
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.
"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.
- ts-r
- Other tools
| Run | Time | Peak memory | Notes |
|---|---|---|---|
| ts-r-lint, same 48 rules, config cached (its default) | 15.5 ms | 9 MB | Skips evaluating the config, which ESLint does on every run, so not drawn next to ESLint |
| ts-r-lint, same 48 rules, config not cached | 733 ms | 336 MB | Peak is the Node process that evaluates the config |
| ESLint, same 48 rules | 1,015 ms | 515 MB | 0 problems from the rules, like ts-r-lint |
| ts-r-lint, same 80 rules with the 32 type-aware ones, parity gate off, empty cache | 952 ms | 335 MB | Not 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 ms | 973 MB | 0 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).
| Run | Time | Peak memory | Notes |
|---|---|---|---|
| tRPC server and client sources: ts-r-lint / no type check | 186 / 20 ms | 148 / 9 MB | Median of 3 runs |
| tRPC: ESLint, type-aware | 2,566 ms | 992 MB | Also runs plugin rules ts-r-lint lacks, so not a like-for-like comparison |
| TypeORM packages/typeorm/src: ts-r-lint / no type check | 810 / 194 ms | 380 / 26 MB | Median of 3 runs |
| TypeORM: ESLint, type-aware | 6,718 ms | 1,748 MB | Most 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 check | 725 / 59 ms | 465 / 17 MB | |
| Playwright core sources: ESLint | 5,441 ms | 2,341 MB | Also runs plugin rules ts-r-lint lacks, so not a like-for-like comparison |
| date-fns pkgs/core/src: ts-r-lint | 76 ms | 14 MB | |
| date-fns pkgs/core/src: oxlint (its own config) | 348 ms | 301 MB | Different 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 [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