Narrowing on the Discriminant
Let TypeScript narrow variants in switch statements.
Narrowing on the Discriminant is a free TypeScript Academy lesson on CoddyKit — lesson 2 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.
Narrowing on the Discriminant
Once you check the discriminant, TypeScript narrows the union to the matching member and unlocks its specific fields. This is the payoff of the pattern.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(s: Shape) {
if (s.kind === "circle") return Math.PI * s.radius ** 2;
return s.side ** 2;
}
console.log(area({ kind: "circle", radius: 2 }).toFixed(2));Switch on the Discriminant
A switch over the discriminant is the cleanest way to handle every variant. Inside each case, the type is narrowed automatically.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "square": return s.side ** 2;
}
}
console.log(area({ kind: "square", side: 5 }));Member-Specific Fields Per Case
In the "circle" case, s.radius is available but s.side is not. TypeScript knows exactly which member you are in.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function describe(s: Shape) {
switch (s.kind) {
case "circle": return "r=" + s.radius;
case "square": return "side=" + s.side;
}
}
console.log(describe({ kind: "circle", radius: 9 }));If-Based Narrowing
You do not need a switch. A simple if comparing the discriminant narrows just as effectively.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function perimeter(s: Shape): number {
if (s.kind === "circle") {
return 2 * Math.PI * s.radius;
}
return 4 * s.side;
}
console.log(perimeter({ kind: "square", side: 3 }));Accessing the Wrong Field Fails
Before narrowing, you cannot touch a member-specific field. TypeScript reports an error because the property does not exist on every member.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function broken(s: Shape) {
// Error: radius does not exist on Square
// return s.radius;
return s.kind;
}
console.log(broken({ kind: "square", side: 1 }));Narrowing With Boolean Discriminants
For a { ok: true } | { ok: false } union, checking if (r.ok) narrows to the success member.
type Result =
| { ok: true; data: string }
| { ok: false; error: string };
function handle(r: Result): string {
if (r.ok) return "Data: " + r.data;
return "Error: " + r.error;
}
console.log(handle({ ok: false, error: "boom" }));Early Return Narrowing
Returning early on one variant narrows the remaining code to the other variants, a clean way to peel off cases one at a time.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function label(s: Shape): string {
if (s.kind === "circle") return "circle";
// s is now narrowed to Square here
return "square with side " + s.side;
}
console.log(label({ kind: "square", side: 8 }));Combining With Other Conditions
You can mix the discriminant check with other logic. TypeScript keeps the narrowing as long as the discriminant condition is true in that branch.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function bigCircle(s: Shape): boolean {
return s.kind === "circle" && s.radius > 100;
}
console.log(bigCircle({ kind: "circle", radius: 150 }));Narrowing in Array Filters
Narrowing works inside callbacks too. Here each shape is checked individually as we iterate.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
const shapes: Shape[] = [
{ kind: "circle", radius: 1 },
{ kind: "square", side: 2 }
];
shapes.forEach(s => {
if (s.kind === "circle") console.log("c", s.radius);
else console.log("s", s.side);
});Switch Returns Stay Type-Safe
When a switch handles every case and returns, TypeScript infers a precise return type from the union of all case results.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function tag(s: Shape) {
switch (s.kind) {
case "circle": return s.radius;
case "square": return s.side;
}
}
console.log(tag({ kind: "circle", radius: 4 }));Putting Narrowing Together
Whether you use switch or if, checking the discriminant is what makes member fields safely accessible. Next we will guarantee every case is handled.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(s: Shape): number {
return s.kind === "circle" ? Math.PI * s.radius ** 2 : s.side ** 2;
}
console.log(Math.round(area({ kind: "circle", radius: 10 })));Quick Check: Narrowing
Test your understanding of discriminant narrowing.
Recap: Narrowing on the Discriminant
You saw that checking the discriminant with switch or if narrows the union to one member, exposing only that member fields. Early returns and combined conditions all preserve narrowing.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
const s: Shape = { kind: "square", side: 7 };
console.log(s.kind === "square" ? s.side : 0);Frequently asked questions
Is the “Narrowing on the Discriminant” lesson free?
Yes — the full text of “Narrowing on the Discriminant” 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 “Narrowing on the Discriminant”?
Let TypeScript narrow variants in switch statements. 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 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Narrowing on the Discriminant” 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
- Building Discriminated Unions
- Narrowing on the Discriminant
- Exhaustiveness Checking with never
- Modeling State Machines