A linter that runs as a tsserver plugin. It reuses the TypeChecker your editor already has β no second process, no AST conversion, no duplicated type-checking.
Zero built-in rules. Rules are plain functions over the TypeScript compiler API.
ESLint runs in its own process and builds its own type information. On large projects this makes "Auto Fix on Save" slow.
TSSLint piggybacks on tsserver. Diagnostics show up in the same path TypeScript errors do, using the same Program instance.
Traditional TSSLint
βββββββββββ βββββββ
βββββββ βββββββ
β IDE β β IDE β
ββββ¬βββ ββββ¬βββ
β β
βββββ΄βββββ βΌ
βΌ βΌ βββββββββββββββββββ
ββββββββ ββββββββ β tsserver β
β ts- β βlinterβ β βββββββββββββ β
βserverβ β β β βTypeCheckerβ β
β β β β β βββββββ¬ββββββ β
β Type β β Type β β β β
β Chk. β β Chk. β β βββββββΌββββββ β
ββββββββ ββββββββ β β TSSLint β β
β βββββββββββββ β
β two type-checkers βββββββββββββββββββ
two parses β one shared pass
TSLint (TS-AST, deprecated 2019) β ESLint took over via typescript-eslint β TSSLint revives the in-process TS-AST approach as a tsserver plugin (2023).
2013 2019 2023
β β β
β β β
TSLint ββββββββββββββββββ deprecated β
β² β
β² β
ESLint βββββββββββββββββββββββ²ββββββββββββββββββββΆ (active)
β² β
β² β
TSSLint β²ββββββββββββββββΆ (tsserver plugin,
revives TS-AST)
| ESLint | TSLint | Oxlint | TSSLint | |
|---|---|---|---|---|
| Runtime | Node, separate process | Node, separate process | Rust, separate process | Node, in tsserver |
| AST | ESTree | TS AST | Native Rust AST | TS AST |
| Type-aware rules | Yes (its own Program) |
Yes (its own Program) |
Yes (via tsgolint, alpha) |
Yes (shared TypeChecker) |
| Built-in rules | Many | Deprecated | Subset of ESLint (+ JS plugins, alpha) | Zero (imports ESLint / TSLint / TSL) |
| Status | Active standard | Deprecated 2019 | Active | Active |
Pick by need. Largest ecosystem β ESLint. Fastest standalone runtime β Oxlint. Type-aware without duplicate type-checking β TSSLint.
npm install @tsslint/config --save-devtsslint.config.ts:
import { defineConfig } from '@tsslint/config';
export default defineConfig({
rules: {
// your rules
},
});VSCode: install the extension.
Other editors: install the plugin and register it in tsconfig.json:
npm install @tsslint/typescript-plugin --save-dev{
"compilerOptions": {
"plugins": [{ "name": "@tsslint/typescript-plugin" }]
}
}A rule is a function. It receives the TypeScript module, the current Program, the SourceFile, and a report() callback.
import { defineRule } from '@tsslint/config';
export default defineRule(({ typescript: ts, file, report }) => {
ts.forEachChild(file, function visit(node) {
if (node.kind === ts.SyntaxKind.DebuggerStatement) {
report('Debugger statement is not allowed.', node.getStart(file), node.getEnd());
}
ts.forEachChild(node, visit);
});
});Touch program only when you need type information β rules that don't are cached aggressively (see Caching).
report() returns a chainable reporter:
report('No console.', node.getStart(file), node.getEnd())
.asError() // default is Message; also: asWarning(), asSuggestion()
.withDeprecated() // strikethrough
.withUnnecessary() // faded
.withFix('Remove call', () => [
{ fileName: file.fileName, textChanges: [{ span: { start, length }, newText: '' }] },
])
.withRefactor('Wrap in if (DEBUG)', () => [/* ... */]);withFix runs automatically as a quick fix; withRefactor shows up under the editor's refactor menu (user-initiated).
vuejs/language-tools tsslint.config.ts.
Rules can nest; the path becomes the rule id:
defineConfig({
rules: {
style: {
'no-debugger': debuggerRule, // reported as "style/no-debugger"
},
},
});defineConfig also accepts an array β each entry can scope rules with include / exclude minimatch patterns.
By default, rules run in a syntax-only mode and their diagnostics are cached on disk under os.tmpdir()/tsslint-cache/. Cache is keyed by file mtime.
The moment a rule reads ctx.program, it switches to type-aware mode for that file and skips the cache (type information depends on more than one file's mtime). To opt a single diagnostic out of caching without going type-aware, call .withoutCache() on the reporter.
Pass --force to the CLI to ignore the cache.
Every report() captures a stack trace. The diagnostic carries a "Related Information" link back to the exact line in your rule that triggered it β β-click in the editor to jump there:
src/index.ts:3:1
3 β debugger;
β ~~~~~~~~~ Debugger statement is not allowed. (tsslint)
β β³ rules/no-debugger.ts:5:7 β-click to open
npm install @tsslint/cli --save-devnpx tsslint --project tsconfig.json
npx tsslint --project tsconfig.json --fix
npx tsslint --project 'packages/*/tsconfig.json' --filter 'src/**/*.ts'Flags:
| Flag | |
|---|---|
--project <glob...> |
TypeScript projects to lint |
--vue-project <glob...> |
Vue projects |
--vue-vine-project <glob...> |
Vue Vine projects |
--mdx-project <glob...> |
MDX projects |
--astro-project <glob...> |
Astro projects |
--ts-macro-project <glob...> |
TS Macro projects |
--filter <glob...> |
Restrict to matching files |
--fix |
Apply fixes |
--force |
Ignore cache |
--failures-only |
Only print diagnostics that affect exit code |
-h, --help |
TSSLint produces diagnostics and edits β it does not format. Run dprint or Prettier after --fix.
The --*-project flags wire in Volar language plugins so framework files (Vue SFCs, MDX, Astro components, etc.) are virtualized as TypeScript before linting. Anything tsserver can see, TSSLint can lint.
.vue βββ
.mdx βββ€ ββββββββββββββββ ββββββββββββββββββββ
.astroβββΌββββΆβ Framework βββββΆβ tsserver βββββΆ diagnostics
.ts βββ β adapters β β β in editor
β β β TypeChecker β
β ββΆ virtual β β + β
β TS file β β TSSLint plugin β
ββββββββββββββββ ββββββββββββββββββββ
Each flag resolves the language plugin from your project's node_modules, so you must install the corresponding package:
| Flag | Required package(s) |
|---|---|
--vue-project |
@vue/language-core or vue-tsc |
--vue-vine-project |
@vue-vine/language-service or vue-vine-tsc |
--mdx-project |
@mdx-js/language-service |
--astro-project |
@astrojs/ts-plugin |
--ts-macro-project |
@ts-macro/language-plugin or @ts-macro/tsc |
npm install @tsslint/compat-eslint --save-dev
npm install @typescript-eslint/eslint-plugin --save-dev # for @typescript-eslint/* rules
npx tsslint-docgen # generates JSDoc for IDE autocompleteFor each non-built-in rule (<plugin>/<rule>), install the matching ESLint plugin (eslint-plugin-<plugin> or @scope/eslint-plugin).
import { defineConfig, importESLintRules } from '@tsslint/config';
export default defineConfig({
rules: {
...await importESLintRules({
'no-unused-vars': true,
'@typescript-eslint/no-explicit-any': 'warn',
}),
},
});npm install tslint --save-dev # required for built-in rules
npx tsslint-docgenimport { defineConfig, importTSLintRules } from '@tsslint/config';
export default defineConfig({
rules: await importTSLintRules({
'no-console': true,
}),
});npm install tsl --save-devimport { defineConfig, fromTSLRules } from '@tsslint/config';
import { core } from 'tsl';
export default defineConfig({
rules: fromTSLRules(core.all()),
});Plugins can rewrite rules per file, filter diagnostics, and inject code fixes. Three are bundled:
import {
defineConfig,
createIgnorePlugin,
createCategoryPlugin,
createDiagnosticsPlugin,
} from '@tsslint/config';
import ts from 'typescript';
export default defineConfig({
rules: { /* ... */ },
plugins: [
// // tsslint-ignore [rule-id] β single-line, or *-start / *-end pairs
createIgnorePlugin('tsslint-ignore', /* report unused */ true),
// Override severity by rule-id pattern
createCategoryPlugin({
'style/*': ts.DiagnosticCategory.Warning,
}),
// Forward TypeScript's own diagnostics through the same pipeline
createDiagnosticsPlugin('semantic'),
],
});Build your own with the Plugin type from @tsslint/types.
- Node.js 22.6.0+ (uses
--experimental-strip-typesto loadtsslint.config.tsdirectly β no transpile step) - Any TypeScript version with Language Service Plugin support
- Not compatible with
typescript-go(v7), which does not yet support Language Service Plugins
