Skip to lesson

learningtypescript.org / basics / 04-control-flow · lesson 4 of 25

TL;DR

Learn TypeScript control flow and type narrowing. Use typeof guards and exhaustive checks to write safer conditional logic.

Key concepts

  • TypeScript control flow
  • type narrowing
  • TypeScript exhaustive check
  • discriminated unions

Control Flow in TypeScript

In this lesson, we'll explore how TypeScript handles program flow control through conditionals and loops. These are essential tools for writing programs that can make decisions and repeat actions.

If Statements

TypeScript uses the same conditional syntax as JavaScript, with added type safety:

const age: number = 18;

if (age >= 18) {
  console.log("You are an adult");
} else {
  console.log("You are a minor");
}

If-Else If-Else

Handle multiple conditions with else if:

const score: number = 85;

if (score >= 90) {
  console.log("Grade: A");
} else if (score >= 80) {
  console.log("Grade: B");
} else if (score >= 70) {
  console.log("Grade: C");
} else if (score >= 60) {
  console.log("Grade: D");
} else {
  console.log("Grade: F");
}

Ternary Operator

For simple conditions, use the ternary operator:

const temperature: number = 25;
const weather: string = temperature > 20 ? "warm" : "cold";

console.log(`It's ${weather} today`);

// Nested ternary (use sparingly for readability)
const category: string =
  temperature > 30 ? "hot" :
  temperature > 20 ? "warm" :
  temperature > 10 ? "cool" : "cold";

console.log(`Temperature category: ${category}`);

Switch Statements

Switch statements provide a clean way to handle multiple cases:

const day: number = 3;
let dayName: string;

switch (day) {
  case 1:
    dayName = "Monday";
    break;
  case 2:
    dayName = "Tuesday";
    break;
  case 3:
    dayName = "Wednesday";
    break;
  case 4:
    dayName = "Thursday";
    break;
  case 5:
    dayName = "Friday";
    break;
  case 6:
  case 7:
    dayName = "Weekend";
    break;
  default:
    dayName = "Invalid day";
}

console.log(`Day ${day} is ${dayName}`);

For Loops

Traditional For Loop

console.log("Counting to 5:");
for (let i = 1; i <= 5; i++) {
  console.log(i);
}

Debug

This loop should add up 1 + 2 + 3 + 4 + 5 and log 'Sum: 15'. Instead it crashes on the very first iteration before printing anything. Predict what goes wrong, then fix it so it logs 'Sum: 15'. (The runner strips types and just runs the code, so this is a real runtime crash, not a compiler complaint.)

let sum = 0;

for (const i = 1; i <= 5; i++) {
sum += i;
}

console.log("Sum:", sum);

Expected output: Sum: 15

Continue learning

For-Of Loop

Iterate over array values:

const fruits: string[] = ["apple", "banana", "orange"];

console.log("Fruits:");
for (const fruit of fruits) {
  console.log(fruit);
}

For-In Loop

Iterate over object keys (use with caution on arrays). The key as keyof typeof person cast below is a preview — it takes the keys of a value's type so person[key] type-checks; you'll learn keyof properly in the generics lesson (07-generics). Read it for now; you'll build it later:

const person = {
  name: "Alice",
  age: 30,
  city: "New York"
};

console.log("Person properties:");
for (const key in person) {
  console.log(`${key}: ${person[key as keyof typeof person]}`);
}

While Loops

Execute code while a condition is true:

let countdown: number = 5;

console.log("Countdown:");
while (countdown > 0) {
  console.log(countdown);
  countdown--;
}
console.log("Blastoff!");

Do-While Loops

Execute code at least once, then check the condition:

let attempts: number = 0;
const maxAttempts: number = 3;

do {
  attempts++;
  console.log(`Attempt ${attempts}`);
} while (attempts < maxAttempts);

console.log("Done!");

Break and Continue

Break

Exit a loop early:

console.log("Finding first number divisible by 7:");
for (let i = 1; i <= 20; i++) {
  if (i % 7 === 0) {
    console.log(`Found: ${i}`);
    break;
  }
}

Continue

Skip to the next iteration:

console.log("Odd numbers from 1 to 10:");
for (let i = 1; i <= 10; i++) {
  if (i % 2 === 0) {
    continue; // Skip even numbers
  }
  console.log(i);
}

Type Guards

TypeScript can narrow types within conditional blocks:

function processValue(value: string | number): void {
  if (typeof value === "string") {
    // TypeScript knows value is a string here
    console.log(`String length: ${value.length}`);
  } else {
    // TypeScript knows value is a number here
    console.log(`Number doubled: ${value * 2}`);
  }
}

processValue("Hello");
processValue(42);

Predict

Inside the else branch, the typeof value === 'string' check has already failed. Given the parameter type string | number, what type has the compiler narrowed value to on the value * 2 line?

function processValue(value: string | number): void {
if (typeof value === "string") {
  console.log(value.length);
} else {
  console.log(value * 2);
}
}
Continue learning

Recall

Without scrolling up: the type guard above hinges on the typeof operator you met in 02-variables-and-types. What kind of value does typeof value evaluate to at runtime, and what would typeof value === 'string' therefore be checking?

Continue learning

Try It Yourself

Practice control flow in the playground below:

// If-else example
const temperature: number = 22;

if (temperature > 30) {
  console.log("It's hot outside!");
} else if (temperature > 20) {
  console.log("It's a pleasant day!");
} else {
  console.log("It's cold outside!");
}

// For loop example
console.log("\nMultiplication table for 5:");
for (let i = 1; i <= 10; i++) {
  console.log(`5 x ${i} = ${5 * i}`);
}

// For-of loop example
const colors: string[] = ["red", "green", "blue"];

console.log("\nColors:");
for (const color of colors) {
  console.log(color);
}

// While loop example
let number: number = 1;
console.log("\nPowers of 2 (up to 64):");
while (number <= 64) {
  console.log(number);
  number *= 2;
}

// Switch statement example
const month: number = 3;
let season: string;

switch (month) {
  case 12:
  case 1:
  case 2:
    season = "Winter";
    break;
  case 3:
  case 4:
  case 5:
    season = "Spring";
    break;
  case 6:
  case 7:
  case 8:
    season = "Summer";
    break;
  case 9:
  case 10:
  case 11:
    season = "Fall";
    break;
  default:
    season = "Unknown";
}

console.log(`\nMonth ${month} is in ${season}`);

// Break and continue example
console.log("\nNumbers from 1 to 10, skipping 5:");
for (let i = 1; i <= 10; i++) {
  if (i === 5) {
    continue;
  }
  console.log(i);
}

Try these exercises:

  1. Write a program that prints the first 10 Fibonacci numbers
  2. Create a function that uses a switch statement to convert day numbers to day names
  3. Use a for loop to calculate the factorial of a number
  4. Write code that uses break to find the first number greater than 100 that's divisible by both 7 and 11
  5. Create a type guard function that handles string, number, and boolean types differently

Reading about conditions and loops is not the same as building with them. This is a build task: a small program that reports its own pass/fail. You are given three empty, fully typed functions to finish, each one a small decision or counting problem. Run it and it fails immediately, telling you which function is still missing. Implement each one until every check passes and it prints All checks passed.

The three functions reuse exactly what this lesson taught: an if/else if chain that returns the first matching result, the modulo operator to test divisibility, and a counting for loop with a running total. The starter already has the data, the stubs, and the checks — you write only the body of each function. Nothing above spells out all three answers, so you will have to assemble them yourself.

Build

Finish the build. Three functions are stubbed out and the checks below them fail until each one returns the right value. Run it as-is to see which check fails first, decide what that function is missing, then implement the three functions until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.

import assert from "node:assert";

// The data: fixed inputs to check against. Do NOT change these values.
const limit: number = 15;
const childAge: number = 8;
const adultAge: number = 40;
const seniorAge: number = 70;

// TODO 1: return a word for one FizzBuzz number.
//   Divisible by 3 AND 5 -> "FizzBuzz"; by 3 only -> "Fizz";
//   by 5 only -> "Buzz"; otherwise the number as a string.
//   fizzbuzzWord(15) -> "FizzBuzz", fizzbuzzWord(9) -> "Fizz", fizzbuzzWord(7) -> "7"
function fizzbuzzWord(n: number): string | undefined {
return undefined; // replace undefined with your code
}

// TODO 2: count how many integers from 1 to limit are divisible by factor.
//   countDivisibleBy(15, 3) -> 5
function countDivisibleBy(limit: number, factor: number): number | undefined {
return undefined; // replace undefined with your code
}

// TODO 3: return a ticket fare for an age.
//   Under 13 -> 5; 65 or older -> 8; everyone else -> 12.
//   fareFor(8) -> 5, fareFor(40) -> 12, fareFor(70) -> 8
function fareFor(age: number): number | undefined {
return undefined; // replace undefined with your code
}

// --- Build checks: these must all pass. Do not edit below this line. ---
assert.strictEqual(
fizzbuzzWord(15),
"FizzBuzz",
"fizzbuzzWord(n) should return 'FizzBuzz' when n is divisible by both 3 and 5",
);
assert.strictEqual(
fizzbuzzWord(9),
"Fizz",
"fizzbuzzWord(n) should return 'Fizz' when n is divisible by 3 only",
);
assert.strictEqual(
fizzbuzzWord(10),
"Buzz",
"fizzbuzzWord(n) should return 'Buzz' when n is divisible by 5 only",
);
assert.strictEqual(
fizzbuzzWord(7),
"7",
"fizzbuzzWord(n) should return the number as a string when it is divisible by neither 3 nor 5",
);
assert.strictEqual(
countDivisibleBy(limit, 3),
5,
"countDivisibleBy(limit, factor) should count how many integers from 1 to limit divide evenly by factor",
);
assert.strictEqual(
fareFor(childAge),
5,
"fareFor(age) should charge 5 for anyone under 13",
);
assert.strictEqual(
fareFor(adultAge),
12,
"fareFor(age) should charge 12 for a standard adult",
);
assert.strictEqual(
fareFor(seniorAge),
8,
"fareFor(age) should charge 8 for anyone 65 or older",
);

console.log("All checks passed.");
console.log("fizzbuzzWord(15):", fizzbuzzWord(15));
console.log("countDivisibleBy(15, 3):", countDivisibleBy(limit, 3));
console.log("fareFor(70):", fareFor(seniorAge));

Expected output: All checks passed. fizzbuzzWord(15): FizzBuzz countDivisibleBy(15, 3): 5 fareFor(70): 8

Continue learning

Once it passes, try two variations and predict each before running:

  1. Reorder the FizzBuzz chain. In fizzbuzzWord, move the divisible-by-3 test (n % 3 === 0 → "Fizz") above the both-divisible test. Predict what fizzbuzzWord(15) returns before running. 15 is divisible by 3 and by 5, but the chain returns on the first match — so it now answers "Fizz" and never reaches "FizzBuzz", and the first check fails. This is the order-matters point in your own hands.
  2. Off-by-one in the count loop. In countDivisibleBy, change the loop bound from i <= limit to i < limit. Decide what countDivisibleBy(15, 3) returns before running. Stopping at 14 drops the multiple 15 itself, so the count comes back 4 instead of 5 and the check fails — the classic inclusive-vs-exclusive loop boundary.

Key Takeaways

  • Use if-else for conditional logic
  • Switch statements are great for multiple discrete values
  • For loops iterate a specific number of times
  • For-of loops iterate over array values
  • While loops continue until a condition is false
  • Break exits a loop, continue skips to the next iteration
  • TypeScript's type system works with control flow for type narrowing

Pro Tip: Choose the right control structure for the job. For-of is great for arrays, while is better when you don't know how many iterations you need, and if-else handles complex conditions!

Next Steps

You now know how to direct program flow with conditionals and loops. Next, you'll learn how to organize the data those loops operate on — arrays, objects, tuples, maps, and sets.

Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.