Mental Model — Mindset Shifts from TypeScript
Five things that will feel different
Section titled “Five things that will feel different”Most Rust frustration for new learners comes from fighting the mental model, not the syntax. TypeScript and Rust look superficially similar — both have types, functions, generics, and pattern matching — but they make fundamentally different trade-offs. Understanding these five shifts up front will save you hours of confusion.
1. Ownership — no garbage collector
Section titled “1. Ownership — no garbage collector”In JavaScript and TypeScript, memory is managed by a garbage collector (GC). You create objects freely; the GC scans for unreachable values and frees them. You never think about allocation.
In Rust, every value has exactly one owner. When the owner goes out of scope, the value is freed — no GC required. This is determined entirely at compile time.
// TypeScript: GC manages memory// You can pass an object anywhere, copy references freelyfunction greet(name: string): string { return `Hello, ${name}!`;}
let s1 = "hello";let s2 = s1; // s1 is still valid — GC tracks both refsconsole.log(s1, s2);fn greet(name: &str) -> String { format!("Hello, {}!", name)}
fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 is *moved* into s2 — s1 is gone println!("{s2}"); // fine // println!("{s1}"); // compile error: value was moved}The key insight: moving a value (assigning, passing to a function) transfers ownership. The old binding is no longer valid. This sounds harsh — but it means the compiler can guarantee at compile time that memory is freed exactly once, no double-frees, no use-after-free.
2. Compile-time memory safety — the borrow checker
Section titled “2. Compile-time memory safety — the borrow checker”Rust’s borrow checker enforces ownership rules. It reads your code at compile time and rejects programs that could cause memory errors. There is no runtime check; there is no null pointer exception; there is no heap corruption.
The rules:
- You can have one mutable reference (
&mut T) or any number of immutable references (&T) — never both at the same time. - References cannot outlive the value they point to.
// TypeScript: no borrow checker — races are possible at runtimelet arr = [1, 2, 3];const ref1 = arr;const ref2 = arr;// both ref1 and ref2 point to the same array// mutating through one is invisible to callers expecting stabilityref1.push(4);console.log(ref2); // [1, 2, 3, 4] — maybe surprisingfn main() { let mut v = vec![1, 2, 3];
let r1 = &v; // immutable borrow let r2 = &v; // second immutable borrow -- fine println!("{r1:?} {r2:?}"); // r1 and r2 are no longer used after this point
let r3 = &mut v; // mutable borrow -- fine here, r1/r2 are done r3.push(4); println!("{r3:?}");}3. Expressions over statements
Section titled “3. Expressions over statements”In TypeScript, if, match, and blocks are statements — they do not produce values. In Rust, almost everything is an expression that produces a value. This means you can assign the result of an if or match directly.
// TypeScript: if is a statement, need ternary for expressionsconst score = 85;const grade = score >= 90 ? "A" : score >= 75 ? "B" : "C";console.log(grade);fn main() { let score = 85;
// if is an expression in Rust -- assign its result directly let grade = if score >= 90 { "A" } else if score >= 75 { "B" } else { "C" };
println!("{grade}");}4. Immutability by default
Section titled “4. Immutability by default”TypeScript’s let creates a mutable binding. Rust’s let creates an immutable binding. You must opt in to mutability with let mut.
// TypeScript: let is mutable by defaultlet count = 0;count = count + 1; // fine, let is mutableconsole.log(count); // 1fn main() { let count = 0; // count = count + 1; // compile error: cannot assign twice to immutable variable
let mut mutable_count = 0; mutable_count += 1; // fine, explicitly mutable println!("{mutable_count}");}Immutability by default is not just a convention (like const in TypeScript) — it is enforced by the compiler. This makes code easier to reason about and enables the borrow checker to work correctly.
5. Errors as values — no exceptions
Section titled “5. Errors as values — no exceptions”TypeScript uses throw and try/catch for error handling. Rust has no exceptions. Errors are returned as values using the Result<T, E> enum. A function that can fail returns Result<Success, Error> and the caller must handle both cases.
// TypeScript: exceptions are thrown, not returnedfunction parseAge(s: string): number { const n = parseInt(s, 10); if (isNaN(n)) throw new Error(`Invalid age: ${s}`); return n;}
try { const age = parseAge("abc"); console.log(age);} catch (e) { console.error(e); // Error: Invalid age: abc}fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("Invalid age '{}': {}", s, e))}
fn main() { match parse_age("25") { Ok(age) => println!("age = {age}"), Err(e) => println!("error: {e}"), }
match parse_age("abc") { Ok(age) => println!("age = {age}"), Err(e) => println!("error: {e}"), }}Try it
Section titled “Try it”Experiment with expressions and Result-based error handling in the browser.
fn parse_number(s: &str) -> Result<i32, String> { s.parse::<i32>().map_err(|e| format!("Cannot parse '{}': {}", s, e))}
fn main() { // expressions: if produces a value let score = 85; let grade = if score >= 90 { "A" } else if score >= 75 { "B" } else { "C" }; println!("grade = {grade}");
// errors as values match parse_number("42") { Ok(n) => println!("parsed: {n}"), Err(e) => println!("error: {e}"), }
match parse_number("oops") { Ok(n) => println!("parsed: {n}"), Err(e) => println!("error: {e}"), }}Compiling…