An extensible static analysis linter for the TypeScript language
—
TSLint includes 163+ built-in rules organized by category and provides comprehensive APIs for developing custom rules.
TSLint rules are organized into five categories:
These rules catch common errors and enforce best practices:
await-promise - Requires that the results of async functions be awaitedban-comma-operator - Disallows the comma operatorban-ts-ignore - Bans @ts-ignore comments from being usedban - Bans the use of specific functions or global methodscurly - Enforces braces for if/for/do/while statementsforin - Requires a for ... in statement to be filtered with an if statementfunction-constructor - Prevents using the built-in Function constructorimport-blacklist - Disallows importing the specified moduleslabel-position - Only allows labels in sensible locationsno-arg - Disallows use of arguments.caller and arguments.calleeno-duplicate-variable - Disallows duplicate variable declarationsno-shadowed-variable - Disallows shadowing variable declarationsno-unused-expression - Disallows unused expression statementsno-unused-variable - Disallows unused imports, variables, functions and private membersno-use-before-declare - Disallows usage of variables before their declarationno-var-keyword - Disallows usage of the var keywordno-var-requires - Disallows the use of require statements except in import statementsno-unsafe-any - Warns when using an expression of type 'any' in unsafe wayno-unsafe-finally - Disallows control flow statements in finally blocksrestrict-plus-operands - Requires both operands of addition to be the same typestrict-boolean-expressions - Restricts the types allowed in boolean expressionsstrict-type-predicates - Warns for type predicates that are always true or always falseuse-isnan - Enforces use of the isNaN() functionRules focused on code maintainability and complexity:
cyclomatic-complexity - Enforces a threshold of cyclomatic complexitydeprecation - Warns when deprecated APIs are usedmax-classes-per-file - Maximum number of classes per filemax-file-line-count - Requires files to remain under a certain number of linesmax-line-length - Requires lines to be under a certain max lengthno-default-export - Disallows default exports in ES6-style modulesno-default-import - Avoid import statements with side-effect importsno-duplicate-imports - Disallows multiple import statements from the same moduleno-mergeable-namespace - Disallows mergeable namespaces in the same fileno-require-imports - Disallows invocation of require()object-literal-sort-keys - Checks ordering of keys in object literalsprefer-const - Requires that variable declarations use const instead of let if possibleprefer-readonly - Requires that private variables are marked as readonly if they're never modified outside of the constructortrailing-comma - Requires or disallows trailing commas in array and object literals, destructuring assignments, function typings, named imports and exports and function parametersRules for consistent coding style:
class-name - Enforces PascalCase for classes and interfacesfile-name-casing - Enforces consistent file naming conventionsinterface-name - Requires interface names to begin with a capital 'I'match-default-export-name - Requires that a default import have the same name as the declaration it importsvariable-name - Checks variable names for various errorscomment-format - Enforces formatting rules for single-line commentscomment-type - Requires or forbids JSDoc style comments for classes, interfaces, functions, etc.completed-docs - Enforces documentation for important items be filled outfile-header - Enforces a certain header comment for all files, matched by a regular expressionjsdoc-format - Enforces basic format rules for JSDoc commentsno-redundant-jsdoc - Forbids JSDoc which duplicates TypeScript functionalityimport-spacing - Ensures proper spacing between import statement elementsno-import-side-effect - Avoid import statements with side-effect importsno-reference - Disallows /// <reference> importsno-reference-import - Don't <reference types="foo" /> if you import foo anywayordered-imports - Requires that import statements be alphabetized and groupedRules for consistent code formatting:
eofline - Ensures the file ends with a newlineindent - Enforces indentation with tabs or spaceslinebreak-style - Enforces consistent linebreak stylesemicolon - Enforces consistent semicolon usagewhitespace - Enforces whitespace style conventionsTSLint provides comprehensive APIs for developing custom rules.
import { Rules } from 'tslint';
// Base rule class
abstract class AbstractRule implements IRule {
static metadata: IRuleMetadata;
constructor(options: IOptions)
getOptions(): IOptions
isEnabled(): boolean
apply(sourceFile: ts.SourceFile): RuleFailure[]
applyWithWalker(walker: IWalker): RuleFailure[]
// Helper method for simple rules
applyWithFunction(
sourceFile: ts.SourceFile,
walkFn: (ctx: WalkContext<T>) => void,
options?: T
): RuleFailure[]
}
// Type-aware rule base class
abstract class TypedRule extends AbstractRule implements ITypedRule {
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[]
}
// Optional type-aware rule base class
abstract class OptionallyTypedRule extends AbstractRule {
apply(sourceFile: ts.SourceFile): RuleFailure[]
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[]
}interface IRuleMetadata {
ruleName: string;
type: "functionality" | "maintainability" | "style" | "typescript" | "formatting";
deprecationMessage?: string;
description: string;
descriptionDetails?: string;
hasFix?: boolean;
optionsDescription: string;
options: any;
optionExamples?: Array<true | any[]> | string[] | Array<{ options: any }>;
rationale?: string;
requiresTypeInfo?: boolean;
typescriptOnly: boolean;
codeExamples?: ICodeExample[];
}
interface ICodeExample {
description: string;
config: string;
pass?: string;
fail?: string;
}interface IOptions {
ruleArguments: any[];
ruleSeverity: "warning" | "error" | "off";
ruleName: string;
disabledIntervals: IDisabledInterval[];
}class RuleFailure {
constructor(
sourceFile: ts.SourceFile,
start: number,
end: number,
failure: string,
ruleName: string,
fix?: Fix
)
// Position methods
getFileName(): string
getStartPosition(): RuleFailurePosition
getEndPosition(): RuleFailurePosition
// Content methods
getFailure(): string
getRuleName(): string
getRuleSeverity(): RuleSeverity
setRuleSeverity(value: RuleSeverity): void
// Fix methods
hasFix(): boolean
getFix(): Fix | undefined
// Serialization
toJson(): IRuleFailureJson
equals(other: RuleFailure): boolean
static compare(a: RuleFailure, b: RuleFailure): number
}
interface RuleFailurePosition {
character: number;
line: number;
position: number;
}
interface IRuleFailureJson {
endPosition: IRuleFailurePositionJson;
failure: string;
fix?: FixJson;
name: string;
ruleSeverity: string;
ruleName: string;
startPosition: IRuleFailurePositionJson;
}
interface IRuleFailurePositionJson {
character: number;
line: number;
position: number;
}
interface ReplacementJson {
innerStart: number;
innerLength: number;
innerText: string;
}
type FixJson = ReplacementJson | ReplacementJson[];
type RuleSeverity = "warning" | "error" | "off";
type Fix = Replacement | Replacement[];class Replacement {
constructor(start: number, length: number, text: string)
// Static factory methods
static replaceNode(node: ts.Node, text: string, sourceFile?: ts.SourceFile): Replacement
static replaceFromTo(start: number, end: number, text: string): Replacement
static deleteText(start: number, length: number): Replacement
static deleteFromTo(start: number, end: number): Replacement
static appendText(start: number, text: string): Replacement
// Application methods
apply(content: string): string
static applyAll(content: string, replacements: Replacement[]): string
static applyFixes(content: string, fixes: Fix[]): string
}import { Rules, RuleFailure, RuleWalker } from 'tslint';
import * as ts from 'typescript';
export class Rule extends Rules.AbstractRule {
public static metadata: Rules.IRuleMetadata = {
ruleName: 'no-console-log',
description: 'Disallows console.log() calls',
optionsDescription: 'Not configurable.',
options: null,
optionExamples: [true],
type: 'functionality',
typescriptOnly: false,
};
public apply(sourceFile: ts.SourceFile): RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Rules.WalkContext<void>) {
function cb(node: ts.Node): void {
if (ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.expression.getText() === 'console' &&
node.expression.name.text === 'log') {
ctx.addFailureAtNode(node, 'console.log() is not allowed');
}
return ts.forEachChild(node, cb);
}
return ts.forEachChild(ctx.sourceFile, cb);
}import { Rules, RuleFailure } from 'tslint';
import * as ts from 'typescript';
interface Options {
maxLength: number;
}
export class Rule extends Rules.AbstractRule {
public static metadata: Rules.IRuleMetadata = {
ruleName: 'max-identifier-length',
description: 'Enforces maximum identifier length',
optionsDescription: 'Maximum identifier length (default: 50)',
options: {
type: 'object',
properties: {
maxLength: { type: 'number' }
}
},
optionExamples: [
[true, { maxLength: 30 }]
],
type: 'style',
typescriptOnly: false,
};
public apply(sourceFile: ts.SourceFile): RuleFailure[] {
const options: Options = {
maxLength: 50,
...this.ruleArguments[0]
};
return this.applyWithFunction(sourceFile, walk, options);
}
}
function walk(ctx: Rules.WalkContext<Options>) {
function cb(node: ts.Node): void {
if (ts.isIdentifier(node) &&
node.text.length > ctx.options.maxLength) {
ctx.addFailureAtNode(
node,
`Identifier '${node.text}' exceeds maximum length of ${ctx.options.maxLength}`
);
}
return ts.forEachChild(node, cb);
}
return ts.forEachChild(ctx.sourceFile, cb);
}import { Rules, RuleFailure } from 'tslint';
import * as ts from 'typescript';
export class Rule extends Rules.TypedRule {
public static metadata: Rules.IRuleMetadata = {
ruleName: 'no-unused-promise',
description: 'Disallows unused Promise return values',
optionsDescription: 'Not configurable.',
options: null,
optionExamples: [true],
type: 'functionality',
typescriptOnly: false,
requiresTypeInfo: true,
};
public applyWithProgram(
sourceFile: ts.SourceFile,
program: ts.Program
): RuleFailure[] {
return this.applyWithFunction(
sourceFile,
walk,
undefined,
program.getTypeChecker()
);
}
}
function walk(ctx: Rules.WalkContext<void>, tc: ts.TypeChecker) {
function cb(node: ts.Node): void {
if (ts.isExpressionStatement(node) &&
ts.isCallExpression(node.expression)) {
const type = tc.getTypeAtLocation(node.expression);
const typeString = tc.typeToString(type);
if (typeString.includes('Promise<')) {
ctx.addFailureAtNode(
node.expression,
'Promise return value is unused'
);
}
}
return ts.forEachChild(node, cb);
}
return ts.forEachChild(ctx.sourceFile, cb);
}import { Rules, RuleFailure, Replacement } from 'tslint';
import * as ts from 'typescript';
export class Rule extends Rules.AbstractRule {
public static metadata: Rules.IRuleMetadata = {
ruleName: 'prefer-const-assertion',
description: 'Prefer const assertion over type annotation',
optionsDescription: 'Not configurable.',
options: null,
optionExamples: [true],
type: 'style',
typescriptOnly: true,
hasFix: true,
};
public apply(sourceFile: ts.SourceFile): RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Rules.WalkContext<void>) {
function cb(node: ts.Node): void {
if (ts.isVariableDeclaration(node) &&
node.type &&
node.initializer &&
ts.isAsExpression(node.initializer)) {
const fix = [
Replacement.deleteFromTo(node.type.pos, node.type.end),
Replacement.replaceNode(
node.initializer,
`${node.initializer.expression.getText()} as const`
)
];
ctx.addFailureAtNode(
node,
'Prefer const assertion over type annotation',
fix
);
}
return ts.forEachChild(node, cb);
}
return ts.forEachChild(ctx.sourceFile, cb);
}TSLint includes a walker system for AST traversal, though applyWithFunction is now preferred.
// Basic walker interface
interface IWalker {
getSourceFile(): ts.SourceFile;
walk(sourceFile: ts.SourceFile): void;
getFailures(): RuleFailure[];
}
// Abstract walker base class
abstract class AbstractWalker<T = undefined> extends WalkContext<T> implements IWalker {
abstract walk(sourceFile: ts.SourceFile): void;
getSourceFile(): ts.SourceFile;
getFailures(): RuleFailure[];
}
// Base syntax walker class
class SyntaxWalker {
walk(node: ts.Node): void;
protected visitNode(node: ts.Node): void;
protected walkChildren(node: ts.Node): void;
}
// Legacy rule walker (deprecated)
class RuleWalker extends SyntaxWalker {
// Prefer applyWithFunction over extending RuleWalker
}class WalkContext<T = undefined> {
constructor(sourceFile: ts.SourceFile, ruleName: string, options?: T)
// Failure reporting
addFailure(start: number, end: number, message: string, fix?: Fix): void
addFailureAt(position: number, width: number, message: string, fix?: Fix): void
addFailureAtNode(node: ts.Node, message: string, fix?: Fix): void
// Properties
readonly sourceFile: ts.SourceFile
readonly failures: RuleFailure[]
readonly options: T | undefined
readonly ruleName: string
}// In tslint.json
{
"rulesDirectory": [
"./custom-rules",
"node_modules/custom-tslint-rules/lib"
],
"rules": {
"my-custom-rule": true
}
}Rules should be exported with PascalCase class names and kebab-case file names:
no-console-log.ts or no-console-log.jsexport class Rule extends Rules.AbstractRule// Package custom rules as npm module
// package.json
{
"name": "my-tslint-rules",
"main": "lib/index.js",
"files": ["lib/"]
}
// lib/noConsoleLogRule.ts
export class Rule extends Rules.AbstractRule {
// Rule implementation
}applyWithFunction instead of extending RuleWalkerfunction walk(ctx: Rules.WalkContext<void>) {
function cb(node: ts.Node): void {
try {
// Rule logic here
if (someCondition(node)) {
ctx.addFailureAtNode(node, 'Rule violation');
}
} catch (error) {
// Log error but don't fail linting
console.warn(`Rule error: ${error.message}`);
}
return ts.forEachChild(node, cb);
}
return ts.forEachChild(ctx.sourceFile, cb);
}Install with Tessl CLI
npx tessl i tessl/npm-tslint