Challenge 4: Mini compiler
- TypeScript course
- Advanced
- Lesson 57/57
- 6 min
Solve a task testing the full advanced level.
Mini compiler: AST + guard + exhaustive
The advanced level in summary: model an AST with a discriminated union, narrow with type guards and switch, enforce exhaustiveness with never, and build the expression tree with a recursive type. Together they yield a mini compiler that is compile-time safe and runtime-correct.
type Expr =
| { kind: "num"; value: number }
| { kind: "add"; left: Expr; right: Expr };
function evalE(e: Expr): number {
switch (e.kind) {
case "num": return e.value;
case "add": return evalE(e.left) + evalE(e.right);
}
}If you add a new node kind, the never check forces every switch to be updated.
Practice Expression evaluator
Write an Expr discriminated union: num (value: number), add (left, right: Expr), mul (left, right: Expr). Write evalE(e: Expr): number with a switch and an exhaustive const _: never = e in the default branch. Build the expression (1 + 2) * 3 and print the result.
Starter code
// Expr: num | add | mul (discriminated union, recursive)
// evalE(e: Expr): number — switch ile daralt, never ile exhaustive yap
// (1 + 2) * 3 ifadesini kur ve sonucu yazdırSample solution
type Expr =
| { kind: "num"; value: number }
| { kind: "add"; left: Expr; right: Expr }
| { kind: "mul"; left: Expr; right: Expr };
function evalE(e: Expr): number {
switch (e.kind) {
case "num":
return e.value;
case "add":
return evalE(e.left) + evalE(e.right);
case "mul":
return evalE(e.left) * evalE(e.right);
default:
const _: never = e;
throw new Error("exhaustive");
}
}
const expr: Expr = { kind: "mul", left: { kind: "add", left: { kind: "num", value: 1 }, right: { kind: "num", value: 2 } }, right: { kind: "num", value: 3 } };
console.log(evalE(expr));Expected output
9
Practice Evaluate with a type guard
Write the same Expr union. Define a type guard isNum(e: Expr): e is { kind: "num"; value: number }. In evalE, narrow first with isNum(e), then handle add and mul. Build the expression 5 + 10 and print the result.
Starter code
// Expr discriminated union (num | add | mul)
// isNum(e: Expr): e is { kind: "num"; value: number } type guard'ı yaz
// evalE içinde isNum ile daralt, toplamı yazdır
// add(5, num(10)) → 15Sample solution
type Expr =
| { kind: "num"; value: number }
| { kind: "add"; left: Expr; right: Expr }
| { kind: "mul"; left: Expr; right: Expr };
function isNum(e: Expr): e is { kind: "num"; value: number } {
return e.kind === "num";
}
function evalE(e: Expr): number {
if (isNum(e)) return e.value;
if (e.kind === "add") return evalE(e.left) + evalE(e.right);
return evalE(e.left) * evalE(e.right);
}
const expr: Expr = { kind: "add", left: { kind: "num", value: 5 }, right: { kind: "num", value: 10 } };
console.log(evalE(expr));Expected output
15
Questions and answers
Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.
1 What does the exhaustive check with `never` guarantee?
- Every case contains a `return`
- The switch never loops forever
- A new variant causes a compile error Correct answer
- The function runs faster
Explanation: The exhaustive `never` check produces a compile error when a new variant is added to the union but the `switch` is not updated, catching missed cases early.
2 Exhaustive variable
function evalE(e: Expr): number {
switch (e.kind) {
case "num": return e.value;
case "add": return evalE(e.left) + evalE(e.right);
default:
const _: ____ = e;
throw new Error("exhaustive");
}
}
function evalE(e: Expr): number {
switch (e.kind) {
case "num": return e.value;
case "add": return evalE(e.left) + evalE(e.right);
default:
const _: never = e;
throw new Error("exhaustive");
}
}
Explanation: For an exhaustive check the variable must be `never`; it is assignable only when `e` is narrowed to `never`. `any` or `unknown` would defeat the check.
3 Compiler parts
| Term | Meaning |
|---|---|
Discriminated union |
AST node types |
switch + case |
type narrowing |
never |
exhaustive check |
Recursive type |
nested expression tree |