TypeScript Academy · Lesson

Building a Simple Linting Tool

Create a custom diagnostic tool using the compiler API.

Lesson 4 of 413 steps

Building a Simple Linting Tool is a free TypeScript Academy lesson on CoddyKit. This is lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, and your progress syncs across the web and the CoddyKit app. The TypeScript Academy course includes 4 lessons in total.

What Is a Linting Tool?

A linting tool analyzes source code to report style or correctness issues. Using the TypeScript compiler API lets you build type-aware rules that ESLint plugins cannot achieve alone.

// Goal: warn when console.log is called in TypeScript files

Project Setup

Create a Node.js script that loads a TypeScript program, traverses the AST, and reports diagnostics.

import ts from "typescript";
import path from "path";

const files = ["src/index.ts"];
const program = ts.createProgram(files, { strict: true });

Defining a Rule

A rule is a function that receives an AST node and returns an optional diagnostic message if the rule is violated.

type Diagnostic = { file: string; line: number; message: string };

function noConsoleLog(node: ts.Node, sf: ts.SourceFile): Diagnostic | null {
  if (!ts.isCallExpression(node)) return null;
  const expr = node.expression.getText(sf);
  if (expr !== "console.log") return null;
  const { line } = sf.getLineAndCharacterOfPosition(node.pos);
  return { file: sf.fileName, line: line + 1, message: "No console.log allowed" };
}

Walking the AST

Write a recursive walk that applies all rules to every node in each source file.

function walk(
  node: ts.Node,
  sf: ts.SourceFile,
  rules: ((n: ts.Node, sf: ts.SourceFile) => Diagnostic | null)[]
): Diagnostic[] {
  const diags: Diagnostic[] = [];
  for (const rule of rules) {
    const d = rule(node, sf);
    if (d) diags.push(d);
  }
  ts.forEachChild(node, child => diags.push(...walk(child, sf, rules)));
  return diags;
}

Running the Linter

Loop over all source files, apply the rules, and print diagnostics.

const rules = [noConsoleLog];
const allDiagnostics: Diagnostic[] = [];

for (const sf of program.getSourceFiles()) {
  if (!sf.isDeclarationFile) {
    allDiagnostics.push(...walk(sf, sf, rules));
  }
}

allDiagnostics.forEach(d =>
  console.log(`${d.file}:${d.line} — ${d.message}`)
);

Adding a Type-Aware Rule

Use the type checker to build rules that depend on the TypeScript type of an expression.

const checker = program.getTypeChecker();

function noAnyReturn(node: ts.Node, sf: ts.SourceFile): Diagnostic | null {
  if (!ts.isFunctionDeclaration(node)) return null;
  const sig = checker.getSignatureFromDeclaration(node);
  if (!sig) return null;
  const ret = checker.getReturnTypeOfSignature(sig);
  if (ret.flags & ts.TypeFlags.Any) {
    const { line } = sf.getLineAndCharacterOfPosition(node.pos);
    return { file: sf.fileName, line: line + 1, message: "Function returns any" };
  }
  return null;
}

Exit Code for CI

Return a non-zero exit code when diagnostics are found so CI pipelines can fail the build.

if (allDiagnostics.length > 0) {
  console.error(`${allDiagnostics.length} lint error(s)`);
  process.exit(1);
}
process.exit(0);

Rule: No Unused Variables

The compiler API exposes unused-variable diagnostics via ts.getPreEmitDiagnostics, augmenting your custom rules with built-in checks.

const preEmit = ts.getPreEmitDiagnostics(program);
preEmit.forEach(d => {
  const msg = ts.flattenDiagnosticMessageText(d.messageText, "
");
  console.log(msg);
});

Rule: Require Return Types

A rule that warns when functions lack explicit return type annotations helps enforce documentation discipline.

function requireReturnType(node: ts.Node, sf: ts.SourceFile): Diagnostic | null {
  if (!ts.isFunctionDeclaration(node)) return null;
  if (!node.type) {
    const { line } = sf.getLineAndCharacterOfPosition(node.pos);
    return { file: sf.fileName, line: line + 1, message: "Missing return type" };
  }
  return null;
}

Comparing to ESLint

Your custom linter complements ESLint. Use ESLint for style rules and your TypeScript compiler API tool for type-aware rules that ESLint cannot express.

// ESLint: stylistic and common pattern rules
// Compiler API linter: deep type reasoning, custom constraints

Recap: Building a Linter

A TypeScript compiler API linter: create a program, walk the AST with rules, optionally query the type checker, and exit non-zero on errors. This approach enables truly type-aware lint rules.

Quick Check

What gives a compiler API linter an advantage over ESLint?

What You Learned

You built a simple TypeScript linter using the compiler API: load a program, walk the AST, apply rules (optionally type-aware), and report diagnostics. This is the foundation for custom static analysis in any TypeScript project.

Free to start

Learn TypeScript with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
101
Lessons
352

Frequently Asked Questions

Is the “Building a Simple Linting Tool” lesson free?

Yes — the full text of “Building a Simple Linting Tool” is free to read here on the web. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO. The TypeScript Academy course includes 4 lessons in total.

What will I learn in “Building a Simple Linting Tool”?

Create a custom diagnostic tool using the compiler API. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start TypeScript Academy?

No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.

How long does the “Building a Simple Linting Tool” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this TypeScript Academy lesson?

Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Creating a TypeScript Program with the API
  2. Traversing the AST with Visitors
  3. Custom Transformers and Code Generation
  4. Building a Simple Linting Tool
← Back to TypeScript Academy