# How to Create a Type-Aware Rule in TSSLint: Accessing the TypeScript Program and TypeChecker

> Learn to create a type-aware TSSLint rule by accessing ctx.program for TypeScript Program and TypeChecker. Automatically run files with full type access for powerful linting.

- Repository: [Johnson Chu/tsslint](https://github.com/johnsoncodehk/tsslint)
- Tags: how-to-guide
- Published: 2026-03-04

---

**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`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/core/index.ts) (specifically lines 125-136), the linter implements the following logic:

- It maintains an internal `rule2Mode` map 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 sets `shouldEnableTypeAware = true`
- It then marks that specific rule as type-aware in the `rule2Mode` map
- Finally, it re-runs the linting pass with the full `Program` and `TypeChecker` available

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`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/types/index.ts) (lines 44-47). This interface exposes the tools needed for both syntax and type analysis:

```typescript
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`:

```typescript
// 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:

```typescript
// 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`](https://github.com/johnsoncodehk/tsslint/blob/main/tsslint.config.ts) file at your project root:

```typescript
// 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`](https://github.com/johnsoncodehk/tsslint/blob/main/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`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/core/index.ts)**: Contains the `createLinter` function and the retry logic that manages `shouldEnableTypeAware` and the `rule2Mode` map. This is where the automatic mode switching occurs.
- **[`packages/types/index.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/types/index.ts)**: Defines the `RuleContext` interface that specifies what data is passed to your rule, including the `program` property.
- **[`packages/typescript-plugin/index.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/typescript-plugin/index.ts)**: The entry point for the VS Code/TypeScript Server plugin that decorates the language service and loads [`tsslint.config.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/tsslint.config.ts).
- **[`fixtures/noConsoleRule.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/fixtures/noConsoleRule.ts)**: A minimal example of a syntax-only rule, useful as a starting template before adding type-aware features.
- **[`tsslint.config.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/tsslint.config.ts)**: The default configuration filename that the plugin searches for in your project directory.

## Summary

- **Access `ctx.program`** inside your rule to automatically trigger type-aware mode; the linter handles the retry mechanism via `rule2Mode` tracking.
- **Use `program.getTypeChecker()`** to inspect types at specific AST locations using TypeScript's native type API.
- **Define rules with `defineRule`** from `@tsslint/config` to receive the standard `RuleContext` object.
- **Register rules in [`tsslint.config.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/tsslint.config.ts)** for 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`](https://github.com/johnsoncodehk/tsslint/blob/main/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`](https://github.com/johnsoncodehk/tsslint/blob/main/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`](https://github.com/johnsoncodehk/tsslint/blob/main/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`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/typescript-plugin/index.ts) automatically discovers this file and loads your rule definitions into the linter.