Lesson 66 of 80
The same `typescript` package that compiles your code exposes a parser and an AST — the exact tool ESLint rules, codemods, and type-aware refactors are built on.
18 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Every .ts file you write goes through a real pipeline before it becomes JavaScript: parse the source text into an abstract syntax tree (a tree of nodes describing every variable, function, and expression), then check and emit from that tree. The typescript npm package — the same one installed as your compiler — exposes that parser directly as the Compiler API. You already depend on it; tsc is a thin CLI wrapper around it.
This matters beyond curiosity: ESLint's TypeScript rules, codemods, and editor features like "rename symbol" or "find all references" aren't magic — they walk this exact AST. Once source code is a tree instead of a string, you can ask precise, structural questions ("every function declaration," "every any type annotation") that a text search or regex could never answer reliably.
import ts from "typescript";
const sourceCode = `
function greet(name: string): string {
return "Hello, " + name;
}
const add = (a: number, b: number): number => a + b;
`;
const sourceFile = ts.createSourceFile(
"example.ts",
sourceCode,
ts.ScriptTarget.Latest,
/* setParentNodes */ true
);
function visit(node: ts.Node): void {
if (ts.isFunctionDeclaration(node) && node.name) {
console.log("function declaration:", node.name.text);
}
if (ts.isVariableDeclaration(node) && node.initializer && ts.isArrowFunction(node.initializer)) {
console.log("arrow function assigned to:", node.name.getText(sourceFile));
}
ts.forEachChild(node, visit); // recurse into every child node
}
visit(sourceFile);
// function declaration: greet
// arrow function assigned to: add
ts.createSourceFile does the same parsing step tsc does internally — it turns the raw text into a ts.SourceFile, the root node of the AST. ts.forEachChild is how you walk it: it doesn't return a flat list, it hands each direct child to your callback and lets you decide whether to recurse further, which is why visit calls itself. ts.isFunctionDeclaration and ts.isArrowFunction are type guards the API ships specifically for narrowing the generic ts.Node down to a specific node shape — the type guards lesson's exact pattern, applied to the compiler's own types.
createSourceFile only parses — it doesn't type-check. The AST has no idea whether add(a, b) is called with the right argument types; that requires building a full ts.Program and asking its TypeChecker for a node's type, a meaningfully heavier setup than a one-off parse.2 + 3 * 4 doesn't parse into a flat sequence — * binds tighter than +, so the root is a BinaryExpression for + whose right child is itself a BinaryExpression for *. Walking it means respecting that nesting, not scanning left to right..then() chain to async/await" or "find every unhandled any" isn't a clever regex — it's a forEachChild walk with the right ts.is* checks, plus (for a codemod that writes changes back) the ts.transform API to produce new source text from the modified tree."How does ESLint know a piece of code is a certain pattern, when a regex would be too fragile?" is a question that separates candidates who've only used tooling from ones who understand how it works — the honest answer is an AST walk against the exact same Compiler API this lesson just used directly.