How to Create a Type-Aware Rule in TSSLint: Accessing the TypeScript Program and TypeChecker
You create a type-aware rule in TSSLint by referencing ctx.program inside your rule callback, which automatically triggers the linter to re-run the file with full TypeScript Program and TypeChecker access.
TSSLint is a high-performance TypeScript linter that supports two execution modes: syntax-only and type-aware. When you create a type-aware rule that needs to inspect actual types—such as detecting usages of any or validating method call signatures—the linter automatically handles the transition from fast syntax checking to full type checking. This article explains the architecture behind this mechanism and provides concrete examples from the johnsoncodehk/tsslint repository.
How TSSLint Handles Type-Aware Execution
TSSLint optimizes performance by running rules in syntax-only mode by default, giving them access only to the parsed SourceFile. However, when a rule attempts to access the TypeScript Program, the linter catches this access attempt and switches strategies.
According to the source code in packages/core/index.ts (specifically lines 125-136), the linter implements the following logic:
- It maintains an internal
rule2Modemap that tracks whether each rule requires type-aware execution - If a rule throws an error while running in syntax-only mode (typically by accessing
ctx.program), the linter setsshouldEnableTypeAware = true - It then marks that specific rule as type-aware in the
rule2Modemap - Finally, it re-runs the linting pass with the full
ProgramandTypeCheckeravailable
You do not need to manually configure this mode or set flags in your configuration file. The architecture automatically detects type dependencies at runtime.
The RuleContext Interface
Every TSSLint rule receives a RuleContext object defined in packages/types/index.ts (lines 44-47). This interface exposes the tools needed for both syntax and type analysis:
export interface RuleContext {
typescript: typeof import('typescript');
program: Program; // only available in type-aware mode
file: SourceFile;
report(message: string, start: number, end: number): Reporter;
}
When operating in syntax-only mode, the program property exists but accessing it triggers the retry mechanism described above. Once the linter switches to type-aware mode, program contains the full TypeScript Program instance, allowing you to call program.getTypeChecker() to obtain a TypeChecker for type introspection.
Creating a Type-Aware Rule: Step-by-Step Examples
Minimal Example: Disallowing the any Type
The following rule demonstrates how to access the TypeChecker to detect variables, parameters, or functions explicitly typed as any:
// src/rules/no-any.ts
import { defineRule } from '@tsslint/config';
export default defineRule(({ typescript: ts, program, file, report }) => {
// TypeChecker is only valid in type-aware mode
const checker = program.getTypeChecker();
ts.forEachChild(file, function walk(node) {
// Look for variable/function/parameter declarations
if (
ts.isVariableDeclaration(node) ||
ts.isParameter(node) ||
ts.isFunctionDeclaration(node) && node.name
) {
const type = checker.getTypeAtLocation(node.name!);
if (type.flags & ts.TypeFlags.Any) {
const start = node.name!.getStart(file);
const end = node.name!.getEnd();
report('Avoid using the `any` type.', start, end)
.withFix('Replace with unknown', () => [
{
fileName: file.fileName,
textChanges: [
{
newText: 'unknown',
span: { start, length: end - start },
},
],
},
]);
}
}
ts.forEachChild(node, walk);
});
});
This rule uses program.getTypeChecker() to query the type of each identifier. When it detects ts.TypeFlags.Any, it reports a diagnostic and provides an automatic fix to replace any with unknown. The first time this rule runs, it will throw when accessing program, causing the linter to retry with type-aware mode enabled.
Advanced Example: Type-Checked Method Calls
For more complex scenarios, you can inspect the types of expressions to enforce specific patterns:
// src/rules/prefer-string-replace.ts
import { defineRule } from '@tsslint/config';
import type * as ts from 'typescript';
export default defineRule(({ typescript: ts, program, file, report }) => {
const checker = program.getTypeChecker();
ts.forEachChild(file, function walk(node) {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
const { name, expression } = node.expression;
if (name.text !== 'replace') return;
const exprType = checker.getTypeAtLocation(expression);
if (exprType.flags & ts.TypeFlags.String) {
// Found a string.replace call – suggest using a regex replace for safety
const start = node.getStart(file);
const end = node.getEnd();
report('Prefer using RegExp.replace for consistent behavior.', start, end)
.withFix('Add /g flag', () => [
{
fileName: file.fileName,
textChanges: [
{
newText: `${node.getText(file)}.replace(/.../g, ...)`,
span: { start, length: end - start },
},
],
},
]);
}
}
ts.forEachChild(node, walk);
});
});
The pattern remains consistent: obtain the TypeChecker from program, inspect types using checker.getTypeAtLocation(), then use report(...).withFix(...) to provide both diagnostics and automated code modifications.
Registering Your Rule
After implementing your rule, register it in a tsslint.config.ts file at your project root:
// tsslint.config.ts
import { defineConfig } from '@tsslint/config';
import noAny from './src/rules/no-any';
import preferReplace from './src/rules/prefer-string-replace';
export default defineConfig({
rules: {
'no-any': noAny,
'prefer-replace': preferReplace,
},
});
The TypeScript plugin (implemented in packages/typescript-plugin/index.ts) automatically discovers and loads this configuration file when you open a TypeScript project in your editor.
Key Implementation Files
Understanding these core files helps when debugging or extending TSSLint:
packages/core/index.ts: Contains thecreateLinterfunction and the retry logic that managesshouldEnableTypeAwareand therule2Modemap. This is where the automatic mode switching occurs.packages/types/index.ts: Defines theRuleContextinterface that specifies what data is passed to your rule, including theprogramproperty.packages/typescript-plugin/index.ts: The entry point for the VS Code/TypeScript Server plugin that decorates the language service and loadstsslint.config.ts.fixtures/noConsoleRule.ts: A minimal example of a syntax-only rule, useful as a starting template before adding type-aware features.tsslint.config.ts: The default configuration filename that the plugin searches for in your project directory.
Summary
- Access
ctx.programinside your rule to automatically trigger type-aware mode; the linter handles the retry mechanism viarule2Modetracking. - Use
program.getTypeChecker()to inspect types at specific AST locations using TypeScript's native type API. - Define rules with
defineRulefrom@tsslint/configto receive the standardRuleContextobject. - Register rules in
tsslint.config.tsfor the TypeScript language service plugin to load them. - Rely on automatic mode detection rather than manual configuration—the linter optimizes performance by only enabling type checking for rules that actually use it.
Frequently Asked Questions
Do I need to manually enable type-aware mode for my TSSLint rule?
No. According to the implementation in packages/core/index.ts, the linter automatically detects when a rule attempts to access ctx.program during syntax-only execution. It catches the resulting error, sets shouldEnableTypeAware = true, updates the internal rule2Mode map to mark your rule as requiring type information, and transparently re-runs the linting pass with the full TypeScript Program.
What properties are available in the RuleContext object?
The RuleContext interface defined in packages/types/index.ts provides four key properties: typescript (the TypeScript module itself), program (the TypeScript Program instance containing the TypeChecker), file (the SourceFile AST node being linted), and report (a function to emit diagnostics that also supports chaining fix builders via .withFix()).
How do I access the TypeChecker inside a TSSLint rule?
Call program.getTypeChecker() on the program object provided in your rule's RuleContext. This returns the TypeScript TypeChecker instance, which exposes methods like getTypeAtLocation() to query the actual TypeScript types of variables, expressions, and identifiers at specific positions in the source file.
Where should I register my custom type-aware rules?
Export your rules from a tsslint.config.ts file in your project root using defineConfig from @tsslint/config, then add them to the rules object with unique keys. The TypeScript plugin entry point in packages/typescript-plugin/index.ts automatically discovers this file and loads your rule definitions into the linter.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →