Skip to content

johnsoncodehk/tsslint

Repository files navigation

TSSLint

TSSLint Logo

npm package Discord License Ask DeepWiki

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.

Why?

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

How it compares

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.

Setup

npm install @tsslint/config --save-dev

tsslint.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" }]
  }
}

Writing rules

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).

Severity, fixes, refactors

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).

Real-world example

vuejs/language-tools tsslint.config.ts.

Organizing rules

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.

Caching

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.

Debugging

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

CLI

npm install @tsslint/cli --save-dev
npx 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.

Framework support

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

Importing ESLint, TSLint, or TSL rules

ESLint

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 autocomplete

For 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',
    }),
  },
});

TSLint

npm install tslint --save-dev      # required for built-in rules
npx tsslint-docgen
import { defineConfig, importTSLintRules } from '@tsslint/config';

export default defineConfig({
  rules: await importTSLintRules({
    'no-console': true,
  }),
});

TSL

npm install tsl --save-dev
import { defineConfig, fromTSLRules } from '@tsslint/config';
import { core } from 'tsl';

export default defineConfig({
  rules: fromTSLRules(core.all()),
});

Plugins

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.

Requirements

  • Node.js 22.6.0+ (uses --experimental-strip-types to load tsslint.config.ts directly β€” 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

License

MIT

About

πŸ”‹βš‘οΈ The lightest TypeScript semantic linting solution in JS

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors