Control Flow
Control flow in TypeScript vs Rust
Section titled “Control flow in TypeScript vs Rust”Rust has the same control-flow constructs you know — if, while, for — but with two key differences: if and match are expressions that produce values, and match is exhaustive (the compiler errors if you miss a case).
if as an expression
Section titled “if as an expression”In TypeScript you reach for the ternary ? : when you need a conditional value. In Rust, if itself is an expression — every branch must produce the same type.
// TypeScriptconst label = score >= 60 ? "pass" : "fail";
// Rustlet label = if score >= 60 { "pass" } else { "fail" };match — exhaustive switch
Section titled “match — exhaustive switch”match in Rust is like TypeScript’s switch, but exhaustive and expression-oriented. The compiler guarantees every possible value is handled. Patterns can be ranges, literals, enums, and more.
loop, while, for
Section titled “loop, while, for”loopruns forever until abreak. Unlike awhile true,loopcan return a value viabreak value.whileworks as expected.for item in collectioniterates over any iterable — no index arithmetic needed.
const score = 85;const grade = score >= 90 ? "A" : score >= 80 ? "B" : "C";console.log(grade); // "B"
// switch (not exhaustive by default)switch (score) { case 100: console.log("perfect"); break; default: console.log("ok");}fn main() { let score = 85; // if is an expression let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" }; println!("{grade}"); // B
// loop with a break value let mut counter = 0; let result = loop { counter += 1; if counter == 5 { break counter * 2; } }; println!("{result}"); // 10
// for over a range for i in 1..=3 { println!("{i}"); }
// match is exhaustive match score { 90..=100 => println!("A"), 80..=89 => println!("B"), _ => println!("C or below"), }}let chains — one condition, several checks
Section titled “let chains — one condition, several checks”In TypeScript you lean on optional chaining and && to test and narrow in a single if. Rust used to make you nest: if let could only appear immediately after if, so every extra check meant another level of indentation.
Since the 2024 edition you can chain let bindings with &&, mixed with ordinary boolean conditions. Each binding is visible to the conditions after it and to the body. The let parts must be at the top level of the condition — || is still not allowed between them.
const cfg = loadConfig();
// TypeScript narrows through optional chaining and &&if (cfg?.retries != null && cfg.retries > 0) { console.log(`retrying ${cfg.retries} times`);}// Before 2024 — nestedif let Some(c) = &cfg { if let Some(n) = c.retries { if n > 0 { println!("retrying {n} times"); } }}
// 2024 edition — one conditionif let Some(c) = &cfg && let Some(n) = c.retries && n > 0{ println!("retrying {n} times");}Let chains are gated on the 2024 edition (they were stabilised in Rust 1.88), because they depend on the if let temporary-scope change that the edition introduced. On an older edition the compiler rejects them with “let chains are only allowed in Rust 2024 or later”.
fn sum_first_two(nums: &[u8]) -> Option<u8> { let mut iter = nums.iter(); if let Some(first) = iter.next() && let Some(second) = iter.next() { first.checked_add(*second) } else { None }}
#[derive(Debug)]struct Config { retries: Option<u32> }
fn main() { println!("{:?}", sum_first_two(&[3, 4, 5])); println!("{:?}", sum_first_two(&[9]));
let cfg = Some(Config { retries: Some(3) }); // One condition, three checks — no nesting. if let Some(c) = &cfg && let Some(n) = c.retries && n > 0 { println!("retrying {n} times"); }}Compiling…
Try it
Section titled “Try it”fn classify(n: i32) -> &'static str { match n { i32::MIN..=-1 => "negative", 0 => "zero", 1..=9 => "small positive", _ => "large positive", }}
fn main() { let score = 85; let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" }; println!("Grade: {grade}");
for n in [0, -3, 5, 42] { println!("{n} is {}", classify(n)); }}Compiling…