Skip to main content

TypeScript Control Flow

Control flow is the order in which individual statements, instructions, or function calls are executed in a program. TypeScript, being a superset of JavaScript, inherits all of JavaScript's control flow structures while adding type safety. Understanding control flow is essential for writing efficient and logical programs.

Introduction to Control Flow

Control flow determines how a program runs based on certain conditions, allowing you to create dynamic and responsive applications. TypeScript enhances JavaScript's control flow with type checking, helping catch errors before runtime.

In this guide, we'll explore:

  • Conditional statements (if/else, switch)
  • Loops (for, while, do-while)
  • Control flow with type guards
  • Error handling
  • Advanced pattern matching techniques

Conditional Statements

If-Else Statements

The most basic form of control flow is the if statement, which executes a block of code if a specified condition is true.

typescript
function checkAge(age: number): string {
if (age >= 18) {
return "You are an adult";
} else {
return "You are a minor";
}
}

// Example usage
const result1 = checkAge(20); // "You are an adult"
const result2 = checkAge(16); // "You are a minor"

You can chain multiple conditions using else if:

typescript
function getLetterGrade(score: number): string {
if (score >= 90) {
return "A";
} else if (score >= 80) {
return "B";
} else if (score >= 70) {
return "C";
} else if (score >= 60) {
return "D";
} else {
return "F";
}
}

// Example usage
console.log(getLetterGrade(95)); // "A"
console.log(getLetterGrade(82)); // "B"
console.log(getLetterGrade(45)); // "F"

Ternary Operator

For simple if-else conditions, you can use the ternary operator for more concise code:

typescript
function isEven(num: number): string {
return num % 2 === 0 ? "Even" : "Odd";
}

console.log(isEven(4)); // "Even"
console.log(isEven(7)); // "Odd"

Switch Statements

For multiple conditions checking the same variable, the switch statement provides a cleaner syntax:

typescript
function getDayName(dayNumber: number): string {
switch (dayNumber) {
case 0:
return "Sunday";
case 1:
return "Monday";
case 2:
return "Tuesday";
case 3:
return "Wednesday";
case 4:
return "Thursday";
case 5:
return "Friday";
case 6:
return "Saturday";
default:
return "Invalid day number";
}
}

console.log(getDayName(1)); // "Monday"
console.log(getDayName(9)); // "Invalid day number"

Loops

For Loops

The traditional for loop is used when you need to iterate a specific number of times:

typescript
function sumNumbers(n: number): number {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
}

console.log(sumNumbers(5)); // 15 (1+2+3+4+5)

For...of Loops

For iterating over arrays or other iterable objects:

typescript
function listFruits(fruits: string[]): void {
for (const fruit of fruits) {
console.log(`I like ${fruit}`);
}
}

listFruits(["apple", "banana", "orange"]);
// Output:
// I like apple
// I like banana
// I like orange

For...in Loops

For iterating over object properties:

typescript
function displayPersonInfo(person: {[key: string]: any}): void {
for (const key in person) {
console.log(`${key}: ${person[key]}`);
}
}

const john = {
name: "John",
age: 30,
occupation: "Developer"
};

displayPersonInfo(john);
// Output:
// name: John
// age: 30
// occupation: Developer

While Loops

Execute code as long as a condition is true:

typescript
function countDown(start: number): number[] {
const result: number[] = [];
let current = start;

while (current > 0) {
result.push(current);
current--;
}

return result;
}

console.log(countDown(5)); // [5, 4, 3, 2, 1]

Do-While Loops

Similar to while loops but guarantees at least one execution:

typescript
function promptUntilValid(): void {
let input: string;

do {
input = prompt("Please enter a number greater than 10") || "";
} while (parseInt(input) <= 10);

console.log("Valid input received:", input);
}

// This will keep prompting until the user enters a number > 10

Type Guards and Control Flow

TypeScript's type system works seamlessly with control flow to provide stronger type safety.

Type Guards

Type guards help narrow down types within conditional blocks:

typescript
function processValue(value: string | number): string {
if (typeof value === "string") {
// TypeScript knows value is a string here
return value.toUpperCase();
} else {
// TypeScript knows value is a number here
return value.toFixed(2);
}
}

console.log(processValue("hello")); // "HELLO"
console.log(processValue(42.123)); // "42.12"

The in Operator

The in operator checks if a property exists on an object:

typescript
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird): void {
if ("swim" in animal) {
// TypeScript knows animal is Fish
animal.swim();
} else {
// TypeScript knows animal is Bird
animal.fly();
}
}

const fish: Fish = { swim: () => console.log("Swimming...") };
const bird: Bird = { fly: () => console.log("Flying...") };

move(fish); // "Swimming..."
move(bird); // "Flying..."

instanceof Checks

For class instances:

typescript
class Car {
drive() { console.log("Driving a car"); }
}

class Motorcycle {
ride() { console.log("Riding a motorcycle"); }
}

function useVehicle(vehicle: Car | Motorcycle): void {
if (vehicle instanceof Car) {
vehicle.drive();
} else {
vehicle.ride();
}
}

useVehicle(new Car()); // "Driving a car"
useVehicle(new Motorcycle()); // "Riding a motorcycle"

Error Handling

Try-Catch Blocks

Use try-catch to handle potential errors:

typescript
function divide(a: number, b: number): number {
try {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
} catch (error) {
console.error("An error occurred:", error.message);
return Infinity; // Return a fallback value
} finally {
console.log("Division operation attempted");
}
}

console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // Infinity (with error message in console)

Real-World Examples

User Authentication Flow

typescript
interface User {
id: number;
username: string;
role: "admin" | "user" | "guest";
}

function handleUserAccess(user: User | null, requestedResource: string): string {
// First check if user exists (authentication)
if (!user) {
return "Please log in to access this resource";
}

// Then check authorization based on role
switch (user.role) {
case "admin":
return `Access granted to ${requestedResource}`;

case "user":
if (requestedResource.startsWith("public") || requestedResource.startsWith("user")) {
return `Access granted to ${requestedResource}`;
}
return "Access denied: insufficient permissions";

case "guest":
if (requestedResource.startsWith("public")) {
return `Access granted to ${requestedResource}`;
}
return "Access denied: guests can only access public resources";

default:
return "Unknown role";
}
}

// Example usage
const adminUser: User = { id: 1, username: "admin1", role: "admin" };
const regularUser: User = { id: 2, username: "user1", role: "user" };
const guestUser: User = { id: 3, username: "guest1", role: "guest" };
const noUser = null;

console.log(handleUserAccess(adminUser, "admin/dashboard")); // Access granted
console.log(handleUserAccess(regularUser, "user/profile")); // Access granted
console.log(handleUserAccess(regularUser, "admin/settings")); // Access denied
console.log(handleUserAccess(guestUser, "public/docs")); // Access granted
console.log(handleUserAccess(noUser, "user/profile")); // Please log in

Form Validation

typescript
interface FormData {
username?: string;
email?: string;
password?: string;
age?: number;
}

function validateForm(form: FormData): string[] {
const errors: string[] = [];

// Check required fields
if (!form.username) {
errors.push("Username is required");
} else if (form.username.length < 3) {
errors.push("Username must be at least 3 characters");
}

if (!form.email) {
errors.push("Email is required");
} else if (!form.email.includes('@')) {
errors.push("Email must be valid");
}

if (!form.password) {
errors.push("Password is required");
} else {
// Complex password validation
const hasUpperCase = /[A-Z]/.test(form.password);
const hasNumber = /[0-9]/.test(form.password);
const isLongEnough = form.password.length >= 8;

if (!hasUpperCase || !hasNumber || !isLongEnough) {
errors.push("Password must have at least 8 characters, include a number and uppercase letter");
}
}

if (form.age !== undefined) {
if (form.age < 18) {
errors.push("Must be at least 18 years old");
} else if (form.age > 120) {
errors.push("Please enter a valid age");
}
}

return errors;
}

// Example usage
const validForm: FormData = {
username: "johndoe",
email: "[email protected]",
password: "Password123",
age: 30
};

const invalidForm: FormData = {
username: "jo",
email: "not-an-email",
password: "password",
age: 15
};

console.log(validateForm(validForm)); // []
console.log(validateForm(invalidForm));
// [
// "Username must be at least 3 characters",
// "Email must be valid",
// "Password must have at least 8 characters, include a number and uppercase letter",
// "Must be at least 18 years old"
// ]

Control Flow Visualization

Here's a simple visualization of a control flow decision tree:

Summary

TypeScript's control flow mechanisms are powerful tools for directing program execution. In this guide, we've explored:

  • Conditional statements (if/else, switch) for decision-making
  • Various loop types for repeated operations
  • Type guards for type-safe code branches
  • Error handling for robust applications
  • Real-world examples showing practical applications

Understanding control flow is fundamental to writing efficient, logical, and maintainable TypeScript code. With TypeScript's added type checking, your control flow will not only be more predictable but also safer.

Additional Resources and Exercises

Exercises

  1. FizzBuzz Implementation: Write a TypeScript function that prints numbers from 1 to n. For multiples of 3, print "Fizz" instead of the number. For multiples of 5, print "Buzz". For numbers which are multiples of both 3 and 5, print "FizzBuzz".

  2. Shopping Cart Discount Calculator: Create a function that calculates discounts for a shopping cart based on multiple conditions (total amount, user type, coupon codes, etc.).

  3. Type-Narrowing Practice: Create a function that accepts different types of inputs and performs different operations based on the input type, using type guards.

Advanced Topics to Explore

  • Discriminated unions for more complex type safety
  • Control flow analysis in TypeScript
  • Exhaustiveness checking with the never type

Resources

Happy coding with TypeScript!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)