0Pricing
TypeScript Academy · Lesson

Building a Simple Linting Tool

Create a custom diagnostic tool using the compiler API.

Building a Simple Linting Tool is a free TypeScript Academy lesson on CoddyKit — 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, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

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.

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, and the TypeScript Academy course includes 4 lessons in total. 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.

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; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

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