How to Migrate TSLint Rules to TSSLint Using `importTSSLintRules`

The importTSLintRules function in @tsslint/config wraps existing TSLint rule classes and exposes them as native TSSLint rules, allowing you to reuse your current rule set without rewriting any code.

TSSLint is a TypeScript-aware linter that runs as a language service plugin, offering faster feedback than traditional CLI-based tools. If you are moving from a legacy TSLint setup, the johnsoncodehk/tsslint repository provides a dedicated compatibility layer—centered on importTSLintRules—that bridges the two ecosystems. This article explains the internal mechanics of that bridge and provides a concrete migration workflow.

How importTSLintRules Works Internally

The migration helper lives in [packages/config/lib/tslint.ts](https://github.com/johnsoncodehk/tsslint/blob/master/packages/config/lib/tslint.ts#L18-L55). When you invoke importTSLintRules, it executes a three-phase pipeline to transform TSLint constructs into TSSLint-compatible functions.

Rule Discovery via getTSLintRulesDirectories

First, the system locates any custom rule directories defined in your existing tslint.json. The helper getTSLintRulesDirectories walks up the file tree from the current working directory, searches for a tslint.json file, and extracts the rulesDirectory array. This ensures that third-party TSLint plugins installed in your project remain discoverable without manual path configuration.

Rule Loading via loadTSLintRule

Next, for every rule name specified in your configuration, loadTSLintRule attempts to resolve the corresponding compiled JavaScript file. It searches in the following order:

  1. Custom directories discovered in the previous step (e.g., my-tslint-rules/NoConsoleRule.js)
  2. The built-in TSLint rule folder (node_modules/tslint/lib/rules)

Once found, the file is require-d and the rule class is returned for instantiation.

API Translation via convertRule

Finally, the raw TSLint class is passed to convertRule. This wrapper:

The result is a plain object where keys are rule identifiers and values are executable TSSLint rule functions. You spread this object directly into your TSSLint configuration.

Step-by-Step Migration Process

Follow these steps to port an existing TSLint setup into TSSLint without code changes:

  1. Ensure TSLint is installed
    Keep tslint in your node_modules so the loader can resolve built-in rules like no-console or member-ordering.

  2. Preserve your tslint.json
    Do not delete your existing configuration. The file must remain accessible (project root or parent directory) so getTSLintRulesDirectories can read the rulesDirectory field if you use custom plugins.

  3. Install TSSLint dependencies
    Add the configuration package (and CLI if desired):

    npm install --save-dev @tsslint/config @tsslint/cli
  4. Create tsslint.config.ts
    Import importTSLintRules and spread its output into the rules map:

    import { defineConfig, importTSLintRules } from '@tsslint/config';
    
    export default defineConfig({
      rules: {
        // Spread the converted TSLint rules
        ...(await importTSLintRules({
          'no-console': true,
          'member-ordering': [true, { order: 'fields-first' }],
        })),
        
        // Add native TSSLint rules here if needed
      },
    });
  5. Run TSSLint
    Execute the linter via the CLI or as a TypeScript language service plugin:

    npx tsslint --project tsconfig.json

Configuration Examples

Legacy tslint.json (Optional)

You only need this file if you rely on custom rule directories. The rules section inside it is not consumed directly, but keeping it helps document the original settings.

{
  "rulesDirectory": ["my-tslint-rules"],
  "rules": {
    "no-console": true,
    "member-ordering": [true, { "order": "fields-first" }]
  }
}

Modern tsslint.config.ts

This is the entry point TSSLint reads at runtime. The importTSLintRules call accepts an object matching the TSLint rules schema.

import { defineConfig, importTSLintRules } from '@tsslint/config';

export default defineConfig({
  rules: {
    ...(await importTSLintRules({
      'no-console': true,
      'member-ordering': [true, { order: 'fields-first' }],
    })),
  },
});

Key Architectural Details

Severity Conversion
When importTSLintRules processes a rule, it calls normalizeRuleSeverity to convert TSLint’s string levels into internal RuleSeverity values. If a rule is set to "off", the wrapper inserts a no-op placeholder rather than omitting the key entirely, preserving the configuration structure.

Performance Caching
The converted rule functions are automatically cached by TSSLint’s core unless they access ctx.program (type-aware rules). This mirrors TSLint’s behavior while maintaining the high-performance promise of the TSSLint engine.

Fix Translation
If the original TSLint rule provides a fix via failure.hasFix(), the wrapper translates it into a TSSLint fix object that integrates with the TypeScript Language Service, allowing editors to apply the suggestion with a single click.

Summary

  • importTSLintRules in @tsslint/config provides a zero-rewrite migration path for TSLint rules.
  • The system discovers custom directories via getTSLintRulesDirectories, loads rule classes via loadTSLintRule, and converts them via convertRule in packages/config/lib/tslint.ts.
  • Severity levels and automatic fixes are mapped transparently to TypeScript diagnostics.
  • You consume the result by spreading the returned object into the rules field of tsslint.config.ts.
  • Rules are cached for performance unless they require the TypeScript program instance.

Frequently Asked Questions

Do I need to rewrite my custom TSLint rules to use them in TSSLint?

No. As long as your custom rules follow the standard TSLint class interface (exporting a class with apply or applyWithProgram methods), importTSLintRules will wrap them automatically. Place them in a directory listed under rulesDirectory in your tslint.json so the loader can find them.

What happens if a TSLint rule is disabled in the configuration?

If you pass a rule with severity "off" (or false), importTSLintRules still includes the key in the output object but assigns it a no-op function. This ensures the rule name remains present in the configuration object while effectively disabling execution, matching TSLint’s behavior.

Can I mix native TSSLint rules with imported TSLint rules?

Yes. The object returned by importTSLintRules is a standard JavaScript object that you can spread into the rules map. You can add native TSSLint rules before or after the spread, and they will coexist in the same linting pass.

Does the migration support TSLint’s type-aware rules?

Yes. Rules that use applyWithProgram are detected and wrapped correctly. However, because they access ctx.program, they bypass the automatic caching layer and run on every program change, similar to how they behaved in the original TSLint architecture.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →