Union Types Without Nested if Soup
Flatten nested ifs with early returns, let a switch tell you when you forgot a case, and use a lookup object for word-to-text mappings. After this, a new status cannot slip through.
A value that can be a string, a number, null or undefined tempts you to write if inside if inside if. It compiles, and nobody can read it. This page turns that into three flat lines, then shows how a union of fixed words can tell you when you forgot a case.
The soup
function formatValue(value: string | number | null | undefined): string {
if (value !== null) {
if (value !== undefined) {
if (typeof value === "string") {
return value.trim();
} else {
return value.toFixed(2);
}
} else {
return "-";
}
} else {
return "-";
}
}
console.log(formatValue(" Ana "), formatValue(7), formatValue(null), formatValue(undefined));
Ana 7.00 - -
It works. The compiler narrows correctly through every level. But you have to hold four conditions in your head to know what the last toFixed is working on.
Step 1: handle the odd cases first and return
Check the cases that end the function early. The rest of the function then has fewer possibilities, and no nesting.
function formatValue(value: string | number | null | undefined): string {
if (value === null || value === undefined) {
return "-";
}
if (typeof value === "string") {
return value.trim();
}
return value.toFixed(2);
}
console.log(formatValue(" Ana "), formatValue(7), formatValue(null), formatValue(undefined));
Ana 7.00 - -
Same output, three checks, no else. After the first if, value is string | number. After the second, it is a number. Each early return removes a member from the union, and the compiler follows. Read it top to bottom and you can stop at any line and know what the value is.
value == null, with two equals signs, is true for both null and undefined. It is the one place many style guides allow the loose comparison. Use it if your team does. Either way, the narrowing works.
Step 2: use a union of words and let the compiler count
When the value is one of a few fixed words, a switch with no default is checked for you.
type Status = "draft" | "sent" | "paid";
function nextStep(status: Status): string {
switch (status) {
case "draft":
return "Send it";
case "sent":
return "Wait for payment";
case "paid":
return "Archive it";
}
}
That compiles because every word has a case. Now add a fourth status and do not touch the function.
type Status = "draft" | "sent" | "paid" | "overdue";
function nextStep(status: Status): string {
switch (status) {
case "draft":
return "Send it";
case "sent":
return "Wait for payment";
case "paid":
return "Archive it";
}
}
switch2.ts(3,36): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
The error is on the function, pointing out that it can finish without returning a string. It is the same check from the missing-return tutorial, now working as a reminder: you added a word, and this function does not know about it yet. If you had written default: return "Unknown", the error would never appear, and "overdue" would quietly get the wrong answer. Leave the default off when you want the reminder.
Step 3: a lookup instead of an if chain
For plain word-to-text mappings, an object is shorter than a switch.
type Status = "draft" | "sent" | "paid" | "overdue";
const steps = {
draft: "Send it",
sent: "Wait for payment",
paid: "Archive it",
};
function nextStep(status: Status): string {
return steps[status];
}
lookup.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'Status' can't be used to index type '{ draft: string; sent: string; paid: string; }'.
Property 'overdue' does not exist on type '{ draft: string; sent: string; paid: string; }'.
The compiler checks that the object has a key for every status. Add overdue: "Call the customer" and the error goes away. The message is longer than most, so read its last line to see which key is missing.
⚠ Warning: Do not "fix" these by casting: steps[status as keyof typeof steps]. That tells the compiler to stop checking, which is the reminder you wanted.
When to use which
- Several kinds of "empty" in one value: flatten with early returns.
- A fixed list of words, each with different behaviour:
switchwith nodefault. - A fixed list of words that only map to text or numbers: an object lookup.
Related course: TypeScript for Beginners.
About
Flatten nested ifs with early returns, let a switch tell you when you forgot a case, and use a lookup object for word-to-text mappings. After this, a new status cannot slip through.
- Responsible
- Jay P
- Last Update
- 10/21/2026
- Reading Time
- 4 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.