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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
- Return Early: Handle special cases with guard clauses instead of deep nesting.
- Be Explicit About Falsy Values: Compare with
undefined,null, or0when those values are valid. - Keep Ternaries Simple: Use them for choosing between two values, not for complex logic.
- End Every Case: Finish switch cases with
breakorreturnand enablenoFallthroughCasesInSwitch. - Make Switches Exhaustive: Use a
nevercheck or aRecordso that new union members are not forgotten.
Source
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
List all TypeScript tutorials.