Skip to content
Commitwise
Menu
Programming

Control flow and pattern matching

Make decisions with if, guard and early returns, repeat work with loops and ranges, and match values against patterns with switch and when, including the ordering, fallthrough and boundary bugs each language makes easy, in Swift, Kotlin, Java and TypeScript.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

Every program is a recipe with decisions in it: if the user is signed in, show the feed; for each item in the cart, add its price; depending on the status code, retry or give up. The syntax for that is the first thing you learn, and the bugs in it are the last thing you stop making: a condition checked in the wrong order, a loop that runs one time too many, a switch that falls into the next case.

New to some of these? Open what you need, or skip ahead.

Basics:What are variables and types?

A variable is a named box that holds a value: let age = 30 (Swift), val age = 30 (Kotlin), int age = 30; (Java), const age = 30 (TypeScript). The type says what kind of value fits in the box: Int / int for whole numbers, Double for decimals, String for text, Bool / Boolean for true or false.

  • let (Swift), val (Kotlin) and final (Java) mean the variable can’t be pointed at another value later. var means it can.
  • Swift, Kotlin and TypeScript can often infer the type from the value, so you don’t have to write it. Java needs it written (or var for local variables). TypeScript’s const is like let / val, and its let is like var.
Basics:What are booleans and comparisons?

A boolean is true or false. Comparisons produce booleans: age >= 18, name == "Ana" (=== in TypeScript), count != 0.

Combine them with and (&&), or (||) and not (!): isSignedIn && !isBanned. The right side of && and || only runs if it’s needed, so user != null && user.isAdmin never reads isAdmin from a missing user.

Part Not started

Making decisions

Think of a sorting machine at a post office. A parcel rolls in; if it’s heavier than 20 kg it goes left, otherwise if it’s international it goes right, otherwise straight on. Each parcel takes exactly one path, and the machine checks the questions in order, stopping at the first “yes”.

if, else if and else work exactly like that.

The interesting parts of conditionals are expression vs statement forms (Swift 5.9 and Kotlin have if/switch/when expressions; Java has switch expressions and the ternary; TypeScript only the ternary), definite initialization (a let/val/final assigned in every branch), and structure: guard clauses that exit early make preconditions explicit and keep the happy path at one indentation level.

func shippingLabel(weightKg: Double, international: Bool) -> String {
    if weightKg > 20 {
        return "Freight"
    } else if international {
        return "International"
    } else {
        return "Standard"
    }
}
fun shippingLabel(weightKg: Double, international: Boolean): String {
    if (weightKg > 20) {
        return "Freight"
    } else if (international) {
        return "International"
    } else {
        return "Standard"
    }
}
String shippingLabel(double weightKg, boolean international) {
    if (weightKg > 20) {
        return "Freight";
    } else if (international) {
        return "International";
    } else {
        return "Standard";
    }
}
function shippingLabel(weightKg: number, international: boolean): string {
  if (weightKg > 20) {
    return "Freight";
  } else if (international) {
    return "International";
  } else {
    return "Standard";
  }
}

The first true condition wins

Spot the bugNobody ever gets an A

Tap the line with the bug.

func grade(_ score: Int) -> String {
    if score >= 50 {
        return "Pass"
    } else if score >= 90 {
        return "A"
    } else {
        return "Fail"
    }
}
fun grade(score: Int): String {
    if (score >= 50) {
        return "Pass"
    } else if (score >= 90) {
        return "A"
    } else {
        return "Fail"
    }
}
String grade(int score) {
    if (score >= 50) {
        return "Pass";
    } else if (score >= 90) {
        return "A";
    } else {
        return "Fail";
    }
}
function grade(score: number): string {
  if (score >= 50) {
    return "Pass";
  } else if (score >= 90) {
    return "A";
  } else {
    return "Fail";
  }
}

Exit early

When a function needs several things to be true before it can do its job, check each one and leave as soon as one fails. The main work then stays at the left edge, instead of inside three nested ifs:

func checkout(cart: Cart?, user: User?) -> String {
    guard let user else { return "Please sign in" }
    guard let cart, !cart.items.isEmpty else { return "Your cart is empty" }
    guard user.hasPaymentMethod else { return "Add a card" }

    return "Charging \(cart.total) to \(user.name)"
}
fun checkout(cart: Cart?, user: User?): String {
    if (user == null) return "Please sign in"
    if (cart == null || cart.items.isEmpty()) return "Your cart is empty"
    if (!user.hasPaymentMethod) return "Add a card"

    return "Charging ${cart.total} to ${user.name}"
}
String checkout(Cart cart, User user) {
    if (user == null) return "Please sign in";
    if (cart == null || cart.items().isEmpty()) return "Your cart is empty";
    if (!user.hasPaymentMethod()) return "Add a card";

    return "Charging " + cart.total() + " to " + user.name();
}
function checkout(cart: Cart | undefined, user: User | undefined): string {
  if (!user) return "Please sign in";
  if (!cart || cart.items.length === 0) return "Your cart is empty";
  if (!user.hasPaymentMethod) return "Add a card";

  return `Charging ${cart.total} to ${user.name}`;
}

Swift has a keyword for this, guard … else { return }: the compiler checks that the else really leaves the function. After a guard let, the unwrapped value is available for the rest of the function. In Kotlin, after if (user == null) return, the compiler knows user isn’t null below it (a smart cast).

Practice: flatten a pyramid

  1. Start with nested checks: if user != nil { if cart != nil { if !cart.items.isEmpty { …charge… } } }. The real work is three levels deep, and each else is far from its if.
  2. Invert the first condition: “if there’s no user, return”. The rest of the function can now assume a user exists.
  3. Repeat for every condition, in the same order, each with its own clear message.
  4. What’s left at the bottom is the happy path, unindented, readable top to bottom.

if as a value

In Swift and Kotlin, if can produce a value directly, so a constant can be set from a decision:

let label = if count == 1 { "1 item" } else { "\(count) items" }   // Swift 5.9+
val label = if (count == 1) "1 item" else "$count items"
String label = count == 1 ? "1 item" : count + " items";            // the ternary operator
const label = count === 1 ? "1 item" : `${count} items`;            // the ternary operator

Conditions in production

  • Short-circuit order is a guard: user != null && user.isAdmin is safe, the reverse isn’t. Expensive checks (a network call, a database query) go after cheap ones.
  • Truthiness in TypeScript: if (!count) is true for 0 as well as undefined. if (!name) treats "" as missing. Use === undefined or == null when 0 and "" are valid values.
  • Boolean parameters hide decisions: render(true, false) says nothing at the call site. Named arguments (Swift labels, Kotlin named args) or a small enum make the branch explicit.
  • Definite assignment: declare let/val/final before an if and assign it in every branch; the compiler proves it’s set exactly once, which beats a var with a placeholder value.
  • In an else-if chain the first true condition wins: most specific first, or use non-overlapping conditions.
  • Guard clauses / early returns for preconditions; Swift guard enforces the exit, Kotlin smart-casts after if (x == null) return.
  • if is an expression in Swift (5.9+) and Kotlin; Java and TypeScript use the ternary.
  • TypeScript truthiness treats 0, "" and NaN as false; compare with undefined/null explicitly.

Check this part

  1. In an if / else if chain, which branch runs when several conditions are true?

  2. What is an early return (guard clause)?

  3. Why prefer guard let user else { return } over wrapping the body in if let user { … }?After if (user == null) return, what does Kotlin know about user below?What do guard clauses mainly buy you in Java code?In TypeScript, why can if (!count) be a bug?

  4. Why put user != null before user.isAdmin in a condition?

if/else picks a branch; in an else-if chain, the first true condition wins, so put the most specific first. In Swift and Kotlin if is also an expression that returns a value. Guard clauses and early returns keep the main path unindented.

Part Not started

Repeating work

let names = ["Ana", "Ben", "Cleo"]
for name in names { print(name) }

for i in 0..<3 { print(i) }              // 0, 1, 2 (end excluded)
for i in 1...3 { print(i) }              // 1, 2, 3 (end included)
for (index, name) in names.enumerated() { print("\(index): \(name)") }

var attempts = 0
while attempts < 3 { attempts += 1 }
val names = listOf("Ana", "Ben", "Cleo")
for (name in names) println(name)

for (i in 0 until 3) println(i)          // 0, 1, 2 (end excluded; also 0..<3)
for (i in 1..3) println(i)               // 1, 2, 3 (end included)
for ((index, name) in names.withIndex()) println("$index: $name")

var attempts = 0
while (attempts < 3) attempts++
List<String> names = List.of("Ana", "Ben", "Cleo");
for (String name : names) System.out.println(name);

for (int i = 0; i < 3; i++) System.out.println(i);    // 0, 1, 2
for (int i = 1; i <= 3; i++) System.out.println(i);   // 1, 2, 3
for (int i = 0; i < names.size(); i++) System.out.println(i + ": " + names.get(i));

int attempts = 0;
while (attempts < 3) attempts++;
const names = ["Ana", "Ben", "Cleo"];
for (const name of names) console.log(name);

for (let i = 0; i < 3; i++) console.log(i);     // 0, 1, 2
for (let i = 1; i <= 3; i++) console.log(i);    // 1, 2, 3
names.forEach((name, index) => console.log(`${index}: ${name}`));

let attempts = 0;
while (attempts < 3) attempts++;
  • Loop over the collection itself when you need the items: no index to get wrong.
  • Loop over a range when you need numbers. The key question is always: is the end included? Swift 0..<3 and Kotlin 0 until 3 stop before 3; Swift 1...3 and Kotlin 1..3 include 3. In Java and TypeScript you write the condition yourself, < or <=.
  • while repeats as long as a condition is true; make sure something inside eventually makes it false.
  • break leaves the loop; continue skips the rest of this round and goes to the next item.

Prefer element iteration and higher-order functions (see Collections and functional operations); index loops are for when the index itself matters (neighbors, in-place writes). Range semantics differ in ways that cause off-by-ones when switching languages: Kotlin .. is closed, ..</until half-open; Swift ... closed, ..< half-open, and constructing 0..<-1 traps; Java and TypeScript put the boundary in the condition.

TypeScript note

Use for…of for arrays. for…in walks over property names, so on an array it gives the indexes as strings ("0", "1"…) and can include extra properties. It’s meant for plain objects.

Predict the output

How many numbers does this loop print?

for i in 0...4 { print(i) }
for (i in 0..4) println(i)
for (int i = 0; i <= 4; i++) System.out.println(i);
for (let i = 0; i <= 4; i++) console.log(i);
Exerciseeasy

FizzBuzz, with a twist

Print the numbers from 1 to n, but print “Fizz” for multiples of 3, “Buzz” for multiples of 5, and “FizzBuzz” for multiples of both. The twist: stop early, after printing the first “FizzBuzz” whose number is greater than limit.

n = 20, limit = 100  → 1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16, 17, Fizz, 19, Buzz
n = 50, limit = 20   → … 14, FizzBuzz, 16, … 29, FizzBuzz   (stops after 30)
›

Check “multiple of both” first: a multiple of 15 is also a multiple of 3, and the first true condition wins. x % 15 == 0 means “divisible by 15”.

›

After printing “FizzBuzz”, check whether the number is greater than limit and break out of the loop.

SolutionTry it yourself first
func fizzBuzz(_ n: Int, limit: Int) {
    for i in 1...n {
        if i % 15 == 0 {
            print("FizzBuzz")
            if i > limit { break }
        } else if i % 3 == 0 {
            print("Fizz")
        } else if i % 5 == 0 {
            print("Buzz")
        } else {
            print(i)
        }
    }
}
fun fizzBuzz(n: Int, limit: Int) {
    for (i in 1..n) {
        when {
            i % 15 == 0 -> {
                println("FizzBuzz")
                if (i > limit) break
            }
            i % 3 == 0 -> println("Fizz")
            i % 5 == 0 -> println("Buzz")
            else -> println(i)
        }
    }
}
void fizzBuzz(int n, int limit) {
    for (int i = 1; i <= n; i++) {
        if (i % 15 == 0) {
            System.out.println("FizzBuzz");
            if (i > limit) break;
        } else if (i % 3 == 0) {
            System.out.println("Fizz");
        } else if (i % 5 == 0) {
            System.out.println("Buzz");
        } else {
            System.out.println(i);
        }
    }
}
function fizzBuzz(n: number, limit: number): void {
  for (let i = 1; i <= n; i++) {
    if (i % 15 === 0) {
      console.log("FizzBuzz");
      if (i > limit) break;
    } else if (i % 3 === 0) {
      console.log("Fizz");
    } else if (i % 5 === 0) {
      console.log("Buzz");
    } else {
      console.log(i);
    }
  }
}

break leaves the for loop, not just the if (or, in Kotlin, the when). Try n = 0: Swift’s 1...0 is an invalid range and crashes, while the other languages simply don’t run the loop. Guarding with if n < 1 { return } makes the Swift version safe.

return inside forEach

Predict the output

What does this print?

func firstNegative(_ nums: [Int]) -> Int? {
    var found: Int? = nil
    nums.forEach { n in
        if n < 0 { found = n; return }   // "stop here"
    }
    return found
}
print(firstNegative([3, -1, -7]) as Any)
fun firstNegative(nums: List<Int>): Int? {
    var found: Int? = null
    nums.forEach { n ->
        if (n < 0) { found = n; return@forEach }   // "stop here"
    }
    return found
}
println(firstNegative(listOf(3, -1, -7)))
Integer firstNegative(List<Integer> nums) {
    Integer[] found = { null };
    nums.forEach(n -> {
        if (n < 0) { found[0] = n; return; }       // "stop here"
    });
    return found[0];
}
// System.out.println(firstNegative(List.of(3, -1, -7)));
function firstNegative(nums: number[]): number | undefined {
  let found: number | undefined;
  nums.forEach((n) => {
    if (n < 0) { found = n; return; }          // "stop here"
  });
  return found;
}
console.log(firstNegative([3, -1, -7]));

Leaving nested loops

To stop an outer loop from inside an inner one, use a label (Swift, Kotlin, Java, TypeScript all have them), or better, move the nested loops into a function and return. Labels are fine in algorithmic code; in application code, a function with a name usually reads better.

outer: for row in grid {
    for cell in row where cell == target {
        print("found"); break outer
    }
}
outer@ for (row in grid) {
    for (cell in row) {
        if (cell == target) { println("found"); break@outer }
    }
}
outer:
for (int[] row : grid) {
    for (int cell : row) {
        if (cell == target) { System.out.println("found"); break outer; }
    }
}
outer: for (const row of grid) {
  for (const cell of row) {
    if (cell === target) { console.log("found"); break outer; }
  }
}
  • Iterate elements; use index loops only when the index matters. Ranges: Swift ..</..., Kotlin until/..</.., Java and TypeScript in the condition. Swift traps on invalid ranges (1...0).
  • return in a forEach callback is continue (Kotlin: return@forEach; a bare return is non-local because forEach is inline). Use loops with break, or first(where:) / firstOrNull / findFirst / find.
  • Labeled break/continue exist in all four; extracting a function is usually clearer.
  • TypeScript: for…of for arrays, never for…in.

Check this part

  1. How many times does for i in 0..<3 run?How many times does for (i in 0 until 3) run?How many times does for (int i = 0; i < 3; i++) run?How many times does for (let i = 0; i < 3; i++) run?

  2. What does continue do inside a loop?

  3. Inside nums.forEach { … return … }, what does return do?Inside nums.forEach { … return@forEach … }, what does it do?Inside nums.forEach(n -> { … return; }), what does return do?Inside nums.forEach((n) => { … return; }), what does return do?

  4. What happens with for i in 1...n when n is 0?What does for (i in 1..n) do when n is 0?What does for (int i = 1; i <= n; i++) do when n is 0?What does for (let i = 1; i <= n; i++) do when n is 0?

Loop over collections directly, and over ranges when you need numbers. Know whether a range includes its end. break leaves a loop, continue skips to the next item, and return inside forEach only skips the current item.

Part Not started

switch, when and patterns

When one value decides between many branches, a switch (Swift, Java, TypeScript) or when (Kotlin) reads better than a long if chain:

func describe(_ statusCode: Int) -> String {
    switch statusCode {
    case 200, 201, 204: return "Success"
    case 300..<400:     return "Redirect"
    case 404:           return "Not found"
    case 400..<500:     return "Client error"
    case 500..<600:     return "Server error"
    default:            return "Unknown"
    }
}
fun describe(statusCode: Int): String = when (statusCode) {
    200, 201, 204 -> "Success"
    in 300..<400 -> "Redirect"
    404 -> "Not found"
    in 400..<500 -> "Client error"
    in 500..<600 -> "Server error"
    else -> "Unknown"
}
String describe(int statusCode) {
    return switch (statusCode) {
        case 200, 201, 204 -> "Success";
        case 404 -> "Not found";
        default -> {
            if (statusCode >= 300 && statusCode < 400) yield "Redirect";
            if (statusCode >= 400 && statusCode < 500) yield "Client error";
            if (statusCode >= 500 && statusCode < 600) yield "Server error";
            yield "Unknown";
        }
    };
}
function describe(statusCode: number): string {
  switch (true) {
    case [200, 201, 204].includes(statusCode): return "Success";
    case statusCode === 404:                   return "Not found";
    case statusCode >= 300 && statusCode < 400: return "Redirect";
    case statusCode >= 400 && statusCode < 500: return "Client error";
    case statusCode >= 500 && statusCode < 600: return "Server error";
    default:                                   return "Unknown";
  }
}
  • One case can list several values (200, 201, 204).
  • Swift and Kotlin can match a range (300..<400, in 300..<400). Java and TypeScript can’t put ranges in case, so they fall back to conditions.
  • Cases are checked in order, so 404 comes before the 400s range, the same rule as in an if chain.
  • default / else handles everything else. Swift and Kotlin insist that every possible value is handled, which is why they need it here.

Modern switch/when is an expression with pattern matching. Swift matches tuples, ranges, enum payloads, type casts (case let x as T) and where guards via ~=. Kotlin’s when matches values, in ranges/collections, is types (with smart casts) and arbitrary conditions (subject-less when), with guard conditions in 2.2. Java 21 adds type and record patterns with when guards in switch, plus exhaustiveness for sealed types. TypeScript has no patterns; it narrows through typeof, in, instanceof and discriminant checks. Enums and sealed types with exhaustiveness are covered in Enums and sealed hierarchies.

Fallthrough

Predict the output

size is "M". What does this print?

switch size {
case "S": print("Small")
case "M": print("Medium")
case "L": print("Large")
default: print("Unknown")
}
when (size) {
    "S" -> println("Small")
    "M" -> println("Medium")
    "L" -> println("Large")
    else -> println("Unknown")
}
switch (size) {
    case "S": System.out.println("Small");
    case "M": System.out.println("Medium");
    case "L": System.out.println("Large"); break;
    default: System.out.println("Unknown");
}
switch (size) {
  case "S": console.log("Small");
  case "M": console.log("Medium");
  case "L": console.log("Large"); break;
  default: console.log("Unknown");
}
Exerciseeasy

Delivery price

Write deliveryPrice(weightKg, zone) with a switch or when:

  • zone "local": 3 EUR up to 2 kg, 5 EUR up to 10 kg, otherwise 9 EUR,
  • zone "national": double the local price,
  • any other zone: return nil / null / undefined (not delivered).

Weights are whole kilograms. Make sure 2 kg and 10 kg land in the right band.

›

Work out the local price first, from the weight, then decide by zone. Check the boundaries: “up to 2 kg” includes 2.

›

Swift: case 0...2, case 3...10. Kotlin: in 0..2, in 3..10. Java and TypeScript: weightKg <= 2, weightKg <= 10 in order.

SolutionTry it yourself first
func deliveryPrice(weightKg: Int, zone: String) -> Int? {
    let local: Int
    switch weightKg {
    case ...2:  local = 3
    case ...10: local = 5
    default:    local = 9
    }
    switch zone {
    case "local":    return local
    case "national": return local * 2
    default:         return nil
    }
}
fun deliveryPrice(weightKg: Int, zone: String): Int? {
    val local = when {
        weightKg <= 2 -> 3
        weightKg <= 10 -> 5
        else -> 9
    }
    return when (zone) {
        "local" -> local
        "national" -> local * 2
        else -> null
    }
}
Integer deliveryPrice(int weightKg, String zone) {
    int local = weightKg <= 2 ? 3 : weightKg <= 10 ? 5 : 9;
    return switch (zone) {
        case "local" -> local;
        case "national" -> local * 2;
        default -> null;
    };
}
function deliveryPrice(weightKg: number, zone: string): number | undefined {
  const local = weightKg <= 2 ? 3 : weightKg <= 10 ? 5 : 9;
  switch (zone) {
    case "local": return local;
    case "national": return local * 2;
    default: return undefined;
  }
}

Checking bands in increasing order means each condition only needs an upper bound: by the time <= 10 is checked, everything up to 2 is already handled. That’s the else-if rule from the first part, used on purpose. Swift’s let local: Int declared without a value and set in every case is checked by the compiler: forget one case and it won’t build.

Boundaries in production

Spot the bugServer errors that are never retried

Tap the line with the bug.

enum Outcome { case success, clientError, serverError, other }

func classify(_ status: Int) -> Outcome {
    switch status {
    case 200...299: return .success
    case 400...500: return .clientError
    case 501...599: return .serverError
    default:        return .other
    }
}
enum class Outcome { SUCCESS, CLIENT_ERROR, SERVER_ERROR, OTHER }

fun classify(status: Int): Outcome = when (status) {
    in 200..299 -> Outcome.SUCCESS
    in 400..500 -> Outcome.CLIENT_ERROR
    in 501..599 -> Outcome.SERVER_ERROR
    else -> Outcome.OTHER
}
enum Outcome { SUCCESS, CLIENT_ERROR, SERVER_ERROR, OTHER }

Outcome classify(int status) {
    if (status >= 200 && status <= 299) return Outcome.SUCCESS;
    if (status >= 400 && status <= 500) return Outcome.CLIENT_ERROR;
    if (status >= 501 && status <= 599) return Outcome.SERVER_ERROR;
    return Outcome.OTHER;
}
type Outcome = "success" | "clientError" | "serverError" | "other";

function classify(status: number): Outcome {
  if (status >= 200 && status <= 299) return "success";
  if (status >= 400 && status <= 500) return "clientError";
  if (status >= 501 && status <= 599) return "serverError";
  return "other";
}

Exercisemedium

Route a deep link

An app opens deep links like /products/42, /products/42/reviews, /orders, /orders/7?tab=tracking. Write route(path) that returns a destination: product details, product reviews, order list, order details (with an optional tab), or “unknown”. Use your language’s pattern matching as far as it goes, and make invalid ids (/products/abc) go to “unknown”.

›

Split the path (without the query) into segments first. Then match on the shape: the number of segments and the literal words.

SolutionTry it yourself first
enum Destination: Equatable {
    case product(Int), reviews(Int), orders, order(Int, tab: String?), unknown
}

func route(_ link: String) -> Destination {
    let parts = link.split(separator: "?", maxSplits: 1)
    let segments = parts.first?.split(separator: "/").map(String.init) ?? []
    let tab = parts.count > 1 ? parts[1].split(separator: "&")
        .first { $0.hasPrefix("tab=") }.map { String($0.dropFirst(4)) } : nil

    switch (segments.count, segments.first, segments.count > 1 ? Int(segments[1]) : nil) {
    case (2, "products", let id?):                              return .product(id)
    case (3, "products", let id?) where segments[2] == "reviews": return .reviews(id)
    case (1, "orders", _):                                      return .orders
    case (2, "orders", let id?):                                return .order(id, tab: tab)
    default:                                                    return .unknown
    }
}
sealed interface Destination {
    data class Product(val id: Int) : Destination
    data class Reviews(val id: Int) : Destination
    data object Orders : Destination
    data class Order(val id: Int, val tab: String?) : Destination
    data object Unknown : Destination
}

fun route(link: String): Destination {
    val (path, query) = link.split("?", limit = 2).let { it[0] to it.getOrNull(1) }
    val tab = query?.split("&")?.firstOrNull { it.startsWith("tab=") }?.removePrefix("tab=")
    val s = path.split("/").filter { it.isNotEmpty() }
    val id = s.getOrNull(1)?.toIntOrNull()
    return when {
        s.size == 2 && s[0] == "products" && id != null -> Destination.Product(id)
        s.size == 3 && s[0] == "products" && s[2] == "reviews" && id != null -> Destination.Reviews(id)
        s == listOf("orders") -> Destination.Orders
        s.size == 2 && s[0] == "orders" && id != null -> Destination.Order(id, tab)
        else -> Destination.Unknown
    }
}
sealed interface Destination {
    record Product(int id) implements Destination {}
    record Reviews(int id) implements Destination {}
    record Orders() implements Destination {}
    record Order(int id, String tab) implements Destination {}
    record Unknown() implements Destination {}
}

record Path(List<String> segments, Integer id, String tab) {}

static Destination route(String link) {
    String[] parts = link.split("\\?", 2);
    List<String> s = Arrays.stream(parts[0].split("/")).filter(x -> !x.isEmpty()).toList();
    Integer id = s.size() > 1 && s.get(1).matches("[0-9]{1,9}") ? Integer.valueOf(s.get(1)) : null;
    String tab = parts.length > 1 ? Arrays.stream(parts[1].split("&"))
            .filter(p -> p.startsWith("tab=")).map(p -> p.substring(4)).findFirst().orElse(null) : null;

    return switch (new Path(s, id, tab)) {
        case Path(var seg, Integer i, var t) when i != null && seg.size() == 2 && seg.get(0).equals("products") -> new Destination.Product(i);
        case Path(var seg, Integer i, var t) when i != null && seg.size() == 3 && seg.get(0).equals("products") && seg.get(2).equals("reviews") -> new Destination.Reviews(i);
        case Path(var seg, var i, var t) when seg.equals(List.of("orders")) -> new Destination.Orders();
        case Path(var seg, Integer i, var t) when i != null && seg.size() == 2 && seg.get(0).equals("orders") -> new Destination.Order(i, t);
        default -> new Destination.Unknown();
    };
}
type Destination =
  | { kind: "product"; id: number }
  | { kind: "reviews"; id: number }
  | { kind: "orders" }
  | { kind: "order"; id: number; tab?: string }
  | { kind: "unknown" };

function route(link: string): Destination {
  const url = new URL(link, "app://local");
  const s = url.pathname.split("/").filter(Boolean);
  const id = /^[0-9]{1,9}$/.test(s[1] ?? "") ? Number(s[1]) : undefined;
  const tab = url.searchParams.get("tab") ?? undefined;

  if (s.length === 2 && s[0] === "products" && id !== undefined) return { kind: "product", id };
  if (s.length === 3 && s[0] === "products" && s[2] === "reviews" && id !== undefined) return { kind: "reviews", id };
  if (s.length === 1 && s[0] === "orders") return { kind: "orders" };
  if (s.length === 2 && s[0] === "orders" && id !== undefined) return { kind: "order", id, tab };
  return { kind: "unknown" };
}
  • Parse first, then match on shape: splitting and id parsing happen once; the match reads like the routing table it is.
  • Swift matches a tuple with literal patterns, optional patterns (let id?) and where guards in one switch. Java 21 record patterns with when guards get close; Kotlin uses a subject-less when; TypeScript has no patterns, so an if chain over a discriminated-union result is idiomatic.
  • Invalid ids fall through to unknown because the id is parsed strictly (Int(...), toIntOrNull, a digits regex) before matching, not trusted as text.
  • In a real app, prefer the platform router (SwiftUI NavigationPath with onOpenURL, Jetpack Navigation deep links, a web router) and keep this logic as a pure, unit-tested function.
  • Swift switch and Kotlin when never fall through and must be exhaustive. Java’s classic case X: and TypeScript’s switch fall through without break; Java’s arrow form doesn’t.
  • Patterns: Swift tuples, ranges, optionals (let x?), casts, where; Kotlin in, is with smart casts, subject-less when; Java 21 type and record patterns with when guards; TypeScript narrowing only.
  • Bands as half-open ranges (400..<500, >= 400 && < 500) can’t overlap or leave gaps; test the boundaries table-driven.
switch (statusCode, retryCount) {
case (500..<600, ..<3): retry()
case (429, _):          backOff()
default:                fail()
}
when {
    statusCode in 500..<600 && retryCount < 3 -> retry()
    statusCode == 429 -> backOff()
    else -> fail()
}
switch (result) {
    case Response(var code, var body) when code >= 500 && code < 600 -> retry();
    case Response(var code, var body) when code == 429 -> backOff();
    default -> fail();
}
if (statusCode >= 500 && statusCode < 600 && retryCount < 3) retry();
else if (statusCode === 429) backOff();
else fail();

Check this part

  1. A Swift switch matches case "M". Does it also run the next case?A Kotlin when matches "M". Does it also run the next branch?A classic Java case "M": without break matches. What happens next?A TypeScript case "M": without break matches. What happens next?

  2. Why is case 404 placed before the 400-to-499 range?

  3. Why write status bands as 400..<500 and 500..<600 instead of 400...500 and 501...599?

  4. Which language matches tuples with literal, range and optional patterns plus where guards in one switch?

switch and when compare a value against cases: exact values, lists, ranges, types and conditions. Swift and Kotlin never fall through; Java's and TypeScript's classic switch does unless you break. Check that range boundaries don't overlap or leave gaps.

Key takeaways

  • In an if / else-if chain the first true condition wins: put the most specific one first.
  • Check requirements at the top and return early, so the main path stays readable.
  • Know whether a range includes its end, and use break and continue to control loops.
  • Swift and Kotlin switch/when never fall through; classic Java and TypeScript switch does unless you break.
  • Guard clauses, if/switch/when expressions and definite assignment keep branching explicit and compiler-checked.
  • return inside forEach is continue; use loops or first/find to stop early.
  • Half-open ranges for bands, tested at the boundaries.
  • Use each language’s pattern matching to the extent it exists, after parsing input into typed values.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.