Skip to content

Mental Model — Mindset Shifts from TypeScript

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.

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
// TypeScript: GC manages memory
// You can pass an object anywhere, copy references freely
function greet(name: string): string {
return `Hello, ${name}!`;
}
let s1 = "hello";
let s2 = s1; // s1 is still valid — GC tracks both refs
console.log(s1, s2);
Rust
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
// TypeScript: no borrow checker — races are possible at runtime
let 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 stability
ref1.push(4);
console.log(ref2); // [1, 2, 3, 4] — maybe surprising
Rust
fn 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:?}");
}

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
// TypeScript: if is a statement, need ternary for expressions
const score = 85;
const grade = score >= 90 ? "A" : score >= 75 ? "B" : "C";
console.log(grade);
Rust
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}");
}

TypeScript’s let creates a mutable binding. Rust’s let creates an immutable binding. You must opt in to mutability with let mut.

TypeScript
// TypeScript: let is mutable by default
let count = 0;
count = count + 1; // fine, let is mutable
console.log(count); // 1
Rust
fn 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.

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
// TypeScript: exceptions are thrown, not returned
function 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
}
Rust
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}"),
}
}

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}"),
}
}
When you assign `let s2 = s1;` in Rust (where s1 is a String), what happens to s1?
How many mutable references to a value can exist at the same time in Rust?
In Rust, `let x = if condition { 1 } else { 2 };` is valid because:
How does Rust signal that a function can fail?