TypeScript Control Flow Structures
As a superset of JavaScript, TypeScript inherits all the standard control flow structures of the language. However, it elevates these structures from mere logic gates to powerful tools for type safety. By leveraging its static type system, TypeScript allows developers to perform type narrowing and exhaustive checking, ensuring that the paths your code takes are not only logically sound but also type-safe.
Understanding these structures is fundamental to writing robust, production-grade TypeScript applications.
Conditional statements allow your program to make decisions based on specific criteria. In TypeScript, these decisions do more than just branch the logic; they inform the compiler about the possible types of your variables.
The if...else Statement
The if...else structure is the most common way to implement branching logic. It evaluates a boolean expression and executes a block of code accordingly.
function getGrade(score: number): string {
if (score >= 90) {
return "Excellent";
} else if (score >= 60) {
return "Pass";
} else {
return "Fail";
}
}
Pro-tip: While JavaScript allows for "truthy" and "falsy" checks (e.g., if (count)), it is often better practice in TypeScript to use explicit comparisons (e.g., if (count > 0)). This improves code readability and prevents accidental bugs caused by implicit type coercion.
The switch Statement and Exhaustiveness Checking
The switch statement is ideal when you need to compare a single expression against multiple discrete values. TypeScript provides a unique advantage here through exhaustive checking using the never type.
type Action = "add" | "remove" | "clear";
function handleAction(action: Action): void {
switch (action) {
case "add":
console.log("Adding item...");
break;
case "remove":
console.log("Removing item...");
break;
case "clear":
console.log("Clearing all items...");
break;
default: {
// This ensures that if a new member is added to 'Action',
// the compiler will throw an error here.
const _exhaustiveCheck: never = action;
return _exhaustiveCheck;
}
}
}
In the example above, if you were to add "reset" to the Action type but forget to update the switch block, TypeScript would flag an error in the default case. This is a powerful defensive programming technique that prevents runtime errors when extending union types.
Iteration and Loops
TypeScript offers several ways to traverse data, and choosing the right one depends on whether you are working with arrays, objects, or iterables.
Standard for Loops
When you need precise control over the index or the iteration step, the classic for loop remains the standard choice.
const scores: number[] = [88, 92, 75];
for (let i = 0; i < scores.length; i++) {
console.log(`Score ${i + 1}: ${scores[i]}`);
}
for...of vs. for...in
It is crucial to distinguish between these two to avoid common pitfalls:
for...of: Iterates over the values of an iterable object (like an Array, Map, or Set). This is the preferred method for traversing arrays.for...in: Iterates over the keys (property names) of an object.
// Using for...of for arrays
for (const score of scores) {
console.log(score);
}
// Using for...in for objects
const user = { name: "Alice", age: 18 };
for (const key in user) {
// We use 'keyof typeof user' to ensure type safety when accessing properties
console.log(`${key}: ${user[key as keyof typeof user]}`);
}
Warning: Avoid using for...in to iterate over arrays. It iterates over property keys (which are strings) rather than numeric indices, and it may include inherited properties, leading to unpredictable behavior.
while and do...while
When the number of iterations is not known beforehand, use while (check condition, then execute) or do...while (execute, then check condition).
let attempts = 0;
while (attempts < 3) {
console.log(`Attempt ${attempts + 1}...`);
attempts++;
}
Additionally, for modern development, consider using declarative array methods like .map(), .filter(), and .forEach(). These often result in cleaner, more readable code by abstracting the mechanics of the loop.
Jump Statements
Jump statements allow you to alter the standard flow of execution within loops or functions.
break: Immediately exits the current loop orswitchblock.continue: Skips the remainder of the current loop iteration and moves to the next one.return: Exits the entire function, optionally returning a value.
for (const score of scores) {
if (score < 60) {
console.log("Failure detected. Stopping check.");
break;
}
if (score === 92) {
continue; // Skip this specific score
}
console.log(`Processing score: ${score}`);
}
While TypeScript supports labeled statements (e.g., outerLoop: for...) to break out of nested loops, use them sparingly, as they can make code logic harder to follow.
The Power of Type Narrowing
The most significant difference between TypeScript and JavaScript in the context of control flow is Control Flow Analysis. As the compiler traverses your conditional logic, it "narrows" the type of a variable based on the checks performed.
function processValue(value: string | number): void {
if (typeof value === "string") {
// Inside this block, TypeScript knows 'value' is a string
console.log(value.toUpperCase());
} else {
// Inside this block, TypeScript knows 'value' must be a number
console.log(value.toFixed(2));
}
}
By using type guards such as typeof, instanceof, or the in operator, you can safely access properties that only exist on specific types within a union. This eliminates the need for manual type assertions (as string), making your code more resilient and self-documenting.
Summary of Best Practices
To write clean and safe TypeScript code, keep these principles in mind:
- Leverage Exhaustiveness: Use
switchstatements with thenevertype to ensure all members of a union are handled. - Choose the Right Loop: Use
for...offor array values andfor...infor object keys. - Embrace Narrowing: Use built-in type guards (
typeof,in) to let the compiler do the heavy lifting instead of using type assertions. - Prefer Declarative Patterns: Use higher-order functions like
.map()and.filter()to simplify complex iteration logic.