Beginner

Control Flow in TypeScript

Learn conditionals, loops, switch statements, and type narrowing for control flow in TypeScript with Docker-ready examples

Control flow determines the order in which your program’s statements execute. Instead of running top to bottom, you can branch based on conditions, repeat work with loops, and select between many cases. TypeScript inherits all of JavaScript’s control flow constructs—if/else, switch, for, while—and adds something JavaScript can’t: control flow analysis. The compiler tracks how conditions narrow a value’s type as execution flows through each branch.

As a multi-paradigm language, TypeScript lets you write imperative loops, functional iteration with array methods, or expression-based conditionals. This tutorial focuses on the core imperative constructs while highlighting where TypeScript’s static type system makes control flow safer than plain JavaScript.

The standout feature is type narrowing. When you check if (typeof x === "string"), TypeScript knows that inside that block, x is a string—and it will let you call string methods without complaint. This connection between runtime checks and compile-time types is what makes TypeScript control flow feel different from JavaScript.

Conditionals: if, else if, else

The if statement runs a block when a condition is truthy. Conditions are evaluated as booleans, and TypeScript flags comparisons that can never be true.

Create a file named conditionals.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function classify(score: number): string {
    if (score >= 90) {
        return "A";
    } else if (score >= 80) {
        return "B";
    } else if (score >= 70) {
        return "C";
    } else {
        return "F";
    }
}

const scores: number[] = [95, 82, 71, 64];

for (const score of scores) {
    console.log(`Score ${score} => Grade ${classify(score)}`);
}

Each else if is checked only when the previous conditions were false, so the first matching branch wins.

Ternary and Logical Operators

For simple either/or choices, the ternary operator condition ? a : b is an expression that returns a value. TypeScript also offers the nullish coalescing operator ?? and optional chaining ?., which are common control-flow tools when dealing with possibly-missing values.

Create a file named ternary.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function statusLabel(active: boolean): string {
    return active ? "online" : "offline";
}

console.log(statusLabel(true));
console.log(statusLabel(false));

// Nullish coalescing: use the right side only when the left is null or undefined
function greet(name: string | null): string {
    const safeName = name ?? "guest";
    return `Welcome, ${safeName}!`;
}

console.log(greet("Ada"));
console.log(greet(null));

Note the difference between ?? and ||: ?? only falls back on null or undefined, while || falls back on any falsy value (including 0 and "").

Switch Statements and Type Narrowing

The switch statement compares a value against multiple case labels. TypeScript narrows the type inside each case, which is especially powerful with union types. Always include a break (or return) to avoid fall-through.

Create a file named switch_flow.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Direction = "north" | "south" | "east" | "west";

function move(direction: Direction): [number, number] {
    switch (direction) {
        case "north":
            return [0, 1];
        case "south":
            return [0, -1];
        case "east":
            return [1, 0];
        case "west":
            return [-1, 0];
        default:
            // TypeScript knows this is unreachable for a valid Direction
            return [0, 0];
    }
}

const moves: Direction[] = ["north", "east", "south", "west"];

for (const dir of moves) {
    const [dx, dy] = move(dir);
    console.log(`${dir} => dx=${dx}, dy=${dy}`);
}

Because Direction is a union of string literals, TypeScript verifies that each case is a valid member. If you misspell a case label, the compiler reports an error.

Loops: for, while, and for…of

TypeScript supports the classic C-style for loop, the while loop, and the for...of loop for iterating over arrays and other iterables. Loop control statements break and continue work as expected.

Create a file named loops.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Classic C-style for loop
let sum = 0;
for (let i = 1; i <= 5; i++) {
    sum += i;
}
console.log(`Sum 1..5 = ${sum}`);

// while loop with break
let countdown = 3;
while (true) {
    console.log(`T-minus ${countdown}`);
    countdown--;
    if (countdown < 0) {
        break;
    }
}

// for...of with continue to skip odd numbers
const numbers: number[] = [1, 2, 3, 4, 5, 6];
const evens: number[] = [];
for (const n of numbers) {
    if (n % 2 !== 0) {
        continue;
    }
    evens.push(n);
}
console.log(`Evens: ${evens.join(", ")}`);

Use for...of to iterate over values directly. For index-based access or arbitrary step sizes, the C-style for loop is clearer.

Running with Docker

You can run each example without installing Node.js by using the official node:22-alpine image. ts-node compiles and runs TypeScript in a single step.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Pull the official image
docker pull node:22-alpine

# Run the conditionals example
docker run --rm -v $(pwd):/app -w /app node:22-alpine sh -c 'npx -y ts-node conditionals.ts'

# Run the other examples by swapping the filename
docker run --rm -v $(pwd):/app -w /app node:22-alpine sh -c 'npx -y ts-node ternary.ts'
docker run --rm -v $(pwd):/app -w /app node:22-alpine sh -c 'npx -y ts-node switch_flow.ts'
docker run --rm -v $(pwd):/app -w /app node:22-alpine sh -c 'npx -y ts-node loops.ts'

Expected Output

Running conditionals.ts:

Score 95 => Grade A
Score 82 => Grade B
Score 71 => Grade C
Score 64 => Grade F

Running ternary.ts:

online
offline
Welcome, Ada!
Welcome, guest!

Running switch_flow.ts:

north => dx=0, dy=1
east => dx=1, dy=0
south => dx=0, dy=-1
west => dx=-1, dy=0

Running loops.ts:

Sum 1..5 = 15
T-minus 3
T-minus 2
T-minus 1
T-minus 0
Evens: 2, 4, 6

Key Concepts

  • Type narrowing — Conditions like typeof x === "string" or checking against union literals refine a value’s type within a branch, giving you safe access to type-specific members.
  • Exhaustive switches — When switching over a string-literal union, TypeScript can verify you handled every case, catching missing branches at compile time.
  • ?? vs || — Nullish coalescing falls back only on null/undefined; logical OR falls back on any falsy value. Choose based on whether 0 and "" are valid.
  • for...of for values — Iterate values directly with for...of; reach for the C-style for loop only when you need an index or custom step.
  • Loop controlbreak exits a loop entirely, while continue skips to the next iteration.
  • Ternary is an expression — Unlike if, the ?: operator returns a value, making it ideal for inline assignments and return statements.
  • Truthiness still applies — TypeScript uses JavaScript’s truthiness rules, but the compiler warns when a condition is always true or always false.

Running Today

All examples can be run using Docker:

docker pull node:22-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining