@ttsc/lint
Lint as compile errors. Format as a write operation. One plugin, one config file, one command pipeline.
$ npx ttsc --noEmit
src/index.ts:1:1 - error TS11966: [no-var] Unexpected var, use let or const instead.
1 var x = 3;
~~~~~~~~~A lint rule. Reported as error TS11966. The same error TSxxxxx shape your editor already underlines, your CI already gates on, and your IDE already takes you to with one click. No second tool, no second config, no second CI step.
Diagnostic code compatibility
Each built-in rule owns one positive numeric code in the reserved TS9000 through TS17999 band. The assignments use an append-only ledger, so a new built-in rule does not renumber existing rules and a removed rule’s code is not reused. LSP diagnostics expose the stable rule ID, such as no-var, in their code field.
The ledger introduction preserved every legacy code that was already unique. For each pre-existing collision group, the alphabetically first rule kept the shared legacy code and every other rule received an available code. Those resolved assignments are now frozen by the same append-only policy.
Rules contributed by another Go package use the same collision-free band. Their codes are deterministic for an unchanged complete contributor set and do not depend on registration order. Changing that contributor set recomputes its assignments and can change contributor codes, but it cannot change a built-in assignment.
The shape of the chapter
- Setup: install and write
lint.config.ts. Five minutes. - VS Code: completion, diagnostics, suggestions, fixes, and formatting through one
ttscserver. - Format: the
formatblock. Quotes, semis, trailing commas, print-width reflow. The Prettier surface. - Rules: the lint rule catalog by category.
If you only have five minutes: read Setup, copy the lint.config.ts starter from there, run ttsc fix once, and let the diagnostics tell you the rest.
What you get
- A format block that covers Prettier’s territory: semis, quotes, trailing commas, a print-width reflow. Replaces
prettier. - 720+ lint rules across 21 families (Core, TypeScript, React, JSX-A11y, Promise, Solid, Unicorn, Testing Library, Jest, Vitest, Playwright, Cypress, Storybook, Next.js, TanStack Query, Regexp, Security, JSDoc, Functional, Boundaries, react-perf). Severity per rule (
error/warning/off). Replaceseslint. - One editor stream for TypeScript-Go completion and diagnostics, lint findings and actions, and completion corpora published by project rules.
Commands
npx ttsc # type-check + lint + format diagnostics
npx ttsc fix # autofix lint + configured format rules, then re-check
npx ttsc format # rewrite source files with the format rules onlyttsc fix is a one-shot project pass. It rejects --watch, single-file mode, and --emit. Fixes write to disk before the recheck runs, so source stays modified even when the command exits non-zero on remaining errors. Recommended flow: ttsc fix locally, commit, then CI runs ttsc --noEmit to gate on cleanliness.
ttsc fix and ttsc format differ on defaults: ttsc format applies the always-on default formatter even when no format block is configured, while ttsc fix applies formatting only from a configured format block. With no format block, ttsc fix is a pure lint autofix pass — add a format block (or run ttsc format) to reformat as well.
Which files are linted
The lint pass reads the TypeScript the type-check pass reads: your tsconfig.json file list, plus every .ts, .tsx, .mts, and .cts source the Program pulled in through an import. In a pnpm workspace where a package publishes ./src/index.ts as its entry, every consumer resolves that package to first-party TypeScript, so a consumer’s run reports on it and a project rule receives it in ctx.Sources.
Your config decides what runs there. A files selector scopes an entry to the paths it names, and a path outside the config’s own directory matches no selector, so a config written as files: ["src/**/*.ts"] reports nothing on an imported sibling. A config with no selector applies its rules everywhere it reads, including that sibling. A global ignores entry removes a file from the walk as well, and its patterns anchor at the config’s directory the same way, so it reaches an imported file inside your project rather than a sibling above it.
Declaration files and JavaScript stay selection-driven. A .d.ts is typings rather than authored source, and JavaScript enters the Program only through allowJs, so both are linted only when your own tsconfig.json selects them. An import never pulls either into scope.
Writes stay inside the project. ttsc format never leaves your own file list at all, so importing a sibling package cannot reformat it. ttsc fix reports on an imported source, because it prints diagnostics when the cascade settles, and applies edits only to the files your tsconfig.json selected. The package that owns the file fixes it from its own run.
What you don’t get (yet)
Custom rule authoring inside @ttsc/lint is plugin-author territory. See Plugin Development · Walkthroughs for how @ttsc/lint is itself written.