ZetCode

TypeScript Conditionals

last modified October 15, 2026

Conditionals choose which code to run depending on a condition. The if statement runs a block when a condition is true, the switch statement selects among many cases, and the ternary operator chooses between two values. These are the same constructs as in JavaScript. TypeScript adds narrowing: inside a branch, the compiler knows what the condition has established, for example that a value is a string or is not undefined.

If-Else Statement

The if statement runs its block when the condition is true; the optional else block runs otherwise.

if_else.ts
function checkAge(age: number): string {
    if (age >= 18) {
        return `${age}: adult`;
    } else {
        return `${age}: minor`;
    }
}

console.log(checkAge(18));
console.log(checkAge(12));

The condition age >= 18 is a comparison that evaluates to a boolean. The function is called twice, so both branches can be seen in the output. Because each branch ends with return, the else could also be omitted.

$ npx tsx if_else.ts
18: adult
12: minor

Else-If Chains

Several conditions are tested in sequence with else if. The first one that is true wins.

else_if.ts
function grade(score: number): string {
    if (score >= 90) {
        return "A";
    } else if (score >= 80) {
        return "B";
    } else if (score >= 70) {
        return "C";
    } else {
        return "F";
    }
}

for (const score of [95, 85, 72, 40]) {
    console.log(score, grade(score));
}

For 85, the first condition is false and the second is true, so the result is B; the remaining conditions are not tested. The order matters: checking score >= 70 first would give every passing score a C.

$ npx tsx else_if.ts
95 A
85 B
72 C
40 F

Truthy and Falsy Values

A condition does not have to be a boolean. Any value is converted to true or false.

truthy.ts
const values: unknown[] = [0, "", null, undefined, NaN, false, "0", " ", [], {}, -1];

for (const value of values) {
    const label = typeof value === "string" || (typeof value === "object" && value !== null)
        ? JSON.stringify(value)
        : String(value);
    console.log(`${label.padEnd(9)} -> ${value ? "truthy" : "falsy"}`);
}

Only 0, "", null, undefined, NaN, and false are falsy (plus 0n and -0). Everything else is truthy, including the string "0", a space, an empty array, and an empty object. A condition such as if (count) therefore treats a count of 0 like a missing count, which is a common bug.

$ npx tsx truthy.ts
0         -> falsy
""        -> falsy
null      -> falsy
undefined -> falsy
NaN       -> falsy
false     -> falsy
"0"       -> truthy
" "       -> truthy
[]        -> truthy
{}        -> truthy
-1        -> truthy

Ternary Operator

The ternary operator condition ? a : b is an expression that evaluates to one of two values.

ternary_operator.ts
function advice(isRaining: boolean, temperature: number): string {
    const clothing = temperature < 10 ? "a coat" : "a jacket";
    return isRaining ? `Bring an umbrella and ${clothing}.` : `Enjoy the weather in ${clothing}.`;
}

console.log(advice(true, 8));
console.log(advice(false, 18));

Because it is an expression, the ternary operator can initialize a const or appear in a template literal or a return statement. It is best kept for simple choices; nested ternaries quickly become hard to read.

$ npx tsx ternary_operator.ts
Bring an umbrella and a coat.
Enjoy the weather in a jacket.

Switch Statement

The switch statement compares a value with several cases using strict equality.

switch_statement.ts
function dayType(day: number): string {
    switch (day) {
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
            return "weekday";
        case 6:
        case 7:
            return "weekend";
        default:
            return "invalid day";
    }
}

function dayName(day: number): string {
    let name: string;

    switch (day) {
        case 1:
            name = "Monday";
            break;
        case 6:
            name = "Saturday";
            break;
        default:
            name = `day ${day}`;
    }

    return name;
}

console.log(dayType(3), dayType(7), dayType(9));
console.log(dayName(1), dayName(6), dayName(4));

Cases without code fall through to the next case, so several values can share one branch, as the weekdays do. Each branch must end with break or return; otherwise execution continues into the following case, which is rarely intended. The noFallthroughCasesInSwitch compiler option reports such mistakes. The default branch handles all other values.

In dayName, every path assigns name, so the compiler allows it to be used after the switch; without the default branch, it would report that the variable is used before being assigned.

$ npx tsx switch_statement.ts
weekday weekend invalid day
Monday Saturday day 4

Guard Clauses

Deeply nested conditions are hard to follow. Handling special cases first and returning early keeps the main logic flat.

guard_clauses.ts
type Driver = { age: number; hasLicense: boolean; suspended: boolean };

function canDrive(driver: Driver): string {
    if (driver.age < 18) {
        return "too young to drive";
    }

    if (!driver.hasLicense) {
        return "needs a license";
    }

    if (driver.suspended) {
        return "license is suspended";
    }

    return "can drive";
}

console.log(canDrive({ age: 16, hasLicense: false, suspended: false }));
console.log(canDrive({ age: 30, hasLicense: true, suspended: true }));
console.log(canDrive({ age: 30, hasLicense: true, suspended: false }));

Each guard clause checks one condition and returns immediately. The same logic with nested if/else blocks would need three levels of indentation. When the function reaches the last line, all special cases are known to be excluded.

$ npx tsx guard_clauses.ts
too young to drive
license is suspended
can drive

Logical Operators in Conditions

The &&, ||, and ! operators combine several conditions into one.

logical_operators.ts
type Session = { loggedIn: boolean; role: "admin" | "editor" | "viewer" };

function access(session: Session): string {
    if (session.loggedIn && session.role === "admin") {
        return "full access";
    } else if (session.loggedIn && (session.role === "editor" || session.role === "viewer")) {
        return "limited access";
    } else if (!session.loggedIn) {
        return "please log in";
    }
    return "unknown";
}

console.log(access({ loggedIn: true, role: "admin" }));
console.log(access({ loggedIn: true, role: "viewer" }));
console.log(access({ loggedIn: false, role: "admin" }));

The && operator requires both conditions, || at least one, and ! negates a condition. Parentheses group the role check, so that it is combined with loggedIn as a whole. Both operators short-circuit: if loggedIn is false, the role is not examined at all.

$ npx tsx logical_operators.ts
full access
limited access
please log in

Narrowing in Conditions

TypeScript uses conditions to narrow the types of variables inside the branches.

narrowing.ts
function describe(value: string | number | null): string {
    if (value === null) {
        return "no value";
    }

    if (typeof value === "string") {
        return `text of length ${value.length}`;
    }

    return `number ${value.toFixed(1)}`;
}

console.log(describe(null));
console.log(describe("hello"));
console.log(describe(3.14159));

After the null check, the type of value is string | number. Inside the second if, it is string, so length is available. At the last line, only number remains and toFixed can be called. See the type guards tutorial for more techniques.

$ npx tsx narrowing.ts
no value
text of length 5
number 3.1

Exhaustive Switch

When a switch covers every member of a union, the compiler can verify that no case is missing.

exhaustive_switch.ts
type Light = "red" | "yellow" | "green";

function action(light: Light): string {
    switch (light) {
        case "red":
            return "stop";
        case "yellow":
            return "slow down";
        case "green":
            return "go";
        default: {
            const unreachable: never = light;
            return unreachable;
        }
    }
}

for (const light of ["red", "yellow", "green"] as const) {
    console.log(light, action(light));
}

In the default branch, all three colors have been handled, so light has the never type and can be assigned to a never variable. If a fourth color were added to Light without a new case, this assignment would become a compile-time error. See the never type tutorial.

$ npx tsx exhaustive_switch.ts
red stop
yellow slow down
green go

Lookup Tables Instead of Conditionals

A long chain of conditions that maps values to results can often be replaced by an object.

lookup.ts
type Plan = "free" | "pro" | "team";

const monthlyPrice: Record<Plan, number> = {
    free: 0,
    pro: 12,
    team: 40,
};

function yearlyCost(plan: Plan): number {
    return monthlyPrice[plan] * 12;
}

for (const plan of ["free", "pro", "team"] as const) {
    console.log(plan, yearlyCost(plan));
}

The Record<Plan, number> type requires a price for every plan, so a forgotten plan is a compile-time error, just like with an exhaustive switch. The data is separated from the logic and can be extended without adding new branches.

$ npx tsx lookup.ts
free 0
pro 144
team 480

Best Practices

Source

MDN: if...else

This tutorial covered conditionals in TypeScript. Besides choosing which code runs, each condition also tells the compiler more about the types of the values involved.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.