Hello World & Running
Hello, World
console.log("Hello, World!"); fn main() {
println!("Hello, World!");
} Rust is compiled ahead of time to a native binary with no runtime or garbage collector. Output goes through the
println! macro (the ! marks a macro, not a function), and execution starts at fn main().Formatted output
const name = "Ada";
const age = 36;
console.log(`${name} is ${age}`); let name = "Ada";
let age = 36;
println!("{name} is {age}"); Rust's
println! uses {} placeholders. Since Rust 2021 you can name a variable inline — {name} — which reads much like a template literal, or use positional {} filled by trailing arguments.Variables & Types
Immutable by default
let count = 3;
count = 4;
const limit = 10;
console.log(count, limit); let mut count = 3;
count = 4;
let limit = 10;
println!("{count} {limit}"); Rust's
let is the opposite of TypeScript's: bindings are immutable by default, and you opt into mutation with let mut. A plain let is closer to const, but it is a binding, not a compile-time constant.Shadowing
const input = "42";
const parsed = Number(input);
console.log(parsed + 1); let input = "42";
let input: i32 = input.parse().unwrap();
println!("{}", input + 1); Rust lets you re-declare a name with another
let, called shadowing — the new binding can even change the type. This is how you cleanly convert a value (string to integer here) while reusing one name, something TypeScript's const forbids.Type inference
const total = 0;
const ratio = 1.5;
console.log(total, ratio); let total = 0; // inferred i32
let ratio = 1.5_f64; // inferred f64
println!("{total} {ratio}"); Like TypeScript, Rust infers most types, so annotations are usually optional. But the inference is statically resolved at compile time — there is no
any escape hatch, and an integer literal defaults to i32 unless the context demands otherwise.Ownership & Borrowing
Move semantics
const first = { name: "Ada" };
const second = first;
console.log(first.name);
console.log(second.name); fn main() {
let first = String::from("Ada");
let second = first; // ownership moves to second
// println!("{first}"); // would not compile: first was moved
println!("{second}");
} This is the idea with no TypeScript equivalent. Assigning a heap value moves ownership; the original binding is no longer usable. Where JavaScript shares one object through two references, Rust enforces a single owner so memory can be freed deterministically without a garbage collector.
Borrowing with references
function lengthOf(text: string): number {
return text.length;
}
const text = "hello";
console.log(lengthOf(text));
console.log(text); fn length_of(text: &String) -> usize {
text.len()
}
fn main() {
let text = String::from("hello");
println!("{}", length_of(&text)); // borrow, do not move
println!("{text}"); // still usable
} To use a value without taking ownership, you borrow it with a reference, written
&. The function gets read access but the caller keeps the value. This lets data be passed around freely while the compiler still tracks who owns it.Mutable references
function append(list: number[], value: number): void {
list.push(value);
}
const numbers = [1, 2];
append(numbers, 3);
console.log(numbers); fn append(list: &mut Vec<i32>, value: i32) {
list.push(value);
}
fn main() {
let mut numbers = vec![1, 2];
append(&mut numbers, 3);
println!("{numbers:?}");
} A
&mut reference grants temporary write access. The borrow checker allows only one mutable reference at a time (or any number of read-only ones), which is how Rust prevents data races and aliasing bugs at compile time — guarantees TypeScript cannot make.Strings
String vs &str
let greeting = "Hello";
greeting += ", World";
console.log(greeting); fn main() {
let mut greeting = String::from("Hello");
greeting.push_str(", World");
let slice: &str = &greeting;
println!("{slice}");
} Rust has two string types:
String is a growable, owned, heap-allocated buffer, and &str is a borrowed view into one (a string literal is a &str). TypeScript's single string blurs this distinction that Rust makes explicit for memory control.String operations
const text = "Hello, World";
console.log(text.toUpperCase());
console.log(text.includes("World"));
console.log(text.length); fn main() {
let text = "Hello, World";
println!("{}", text.to_uppercase());
println!("{}", text.contains("World"));
println!("{}", text.chars().count());
} Method names are
snake_case. Note chars().count() rather than a length property: Rust strings are UTF-8, so it counts Unicode scalar values, while .len() would return the byte count.Numbers
Sized integer types
const small = 200;
const big = 2 ** 40;
console.log(small, big); fn main() {
let small: u8 = 200;
let big: i64 = 1 << 40;
println!("{small} {big}");
} Rust replaces TypeScript's single
number with a precise family — i8/u8 through i128/u128, plus f32 and f64. The type is part of the contract, and a debug build panics on overflow rather than wrapping silently.No implicit numeric coercion
const count = 3;
const ratio = count / 2;
console.log(ratio); fn main() {
let count: i32 = 3;
let ratio = count as f64 / 2.0;
println!("{ratio}");
} Rust never mixes numeric types automatically, not even
i32 with f64. You convert explicitly with as. Integer division still truncates, so casting to f64 first is what yields 1.5 instead of 1.Vectors & Arrays
Vectors
const numbers: number[] = [1, 2, 3];
numbers.push(4);
console.log(numbers[0], numbers.length); fn main() {
let mut numbers: Vec<i32> = vec![1, 2, 3];
numbers.push(4);
println!("{} {}", numbers[0], numbers.len());
} A
Vec<T> is Rust's growable list, built with the vec! macro. Every element shares one type. It must be mut to push, and indexing out of bounds panics rather than returning undefined.Iterator adapters
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const total = numbers.reduce((sum, n) => sum + n, 0);
console.log(doubled, evens, total); fn main() {
let numbers = vec![1, 2, 3, 4];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
let evens: Vec<i32> = numbers.iter().filter(|n| *n % 2 == 0).copied().collect();
let total: i32 = numbers.iter().sum();
println!("{doubled:?} {evens:?} {total}");
} Rust's
map/filter are lazy iterator adapters; nothing runs until a consumer like collect or sum drives them. Closures use |n| bars instead of arrow syntax, and the target type of collect is what tells Rust to build a Vec.Hash Maps
Hash maps
const ages: Record<string, number> = {};
ages["ada"] = 36;
console.log(ages["ada"]); use std::collections::HashMap;
fn main() {
let mut ages: HashMap<String, i32> = HashMap::new();
ages.insert(String::from("ada"), 36);
println!("{}", ages["ada"]);
} Rust's key/value store is
HashMap<K, V> from the standard library, brought in with use. Both types are fixed, you insert rather than assign, and it is a pure collection — not an open object you can attach arbitrary fields to.The entry API
const counts: Record<string, number> = {};
for (const letter of "banana") {
counts[letter] = (counts[letter] ?? 0) + 1;
}
console.log(counts); use std::collections::HashMap;
fn main() {
let mut counts: HashMap<char, i32> = HashMap::new();
for letter in "banana".chars() {
*counts.entry(letter).or_insert(0) += 1;
}
println!("{counts:?}");
} The
entry API replaces the ?? 0 dance: or_insert(0) returns a mutable reference to the existing value or inserts a default first. Dereferencing it with * and adding one is the idiomatic Rust way to count occurrences.Control Flow
if is an expression
const hour = 14;
const greeting = hour < 12 ? "morning" : "afternoon";
console.log(greeting); fn main() {
let hour = 14;
let greeting = if hour < 12 { "morning" } else { "afternoon" };
println!("{greeting}");
} In Rust
if is an expression that yields a value, so it replaces the ternary operator (which Rust does not have). Both branches must produce the same type, and the final expression in each block — with no semicolon — is its value.Loops
for (let index = 0; index < 3; index++) {
console.log(index);
}
for (const value of [10, 20]) {
console.log(value);
} fn main() {
for index in 0..3 {
println!("{index}");
}
for value in [10, 20] {
println!("{value}");
}
} Rust has no C-style counting
for. You iterate over a range (0..3, end-exclusive) or any iterable with for x in. There is also a bare loop keyword for an infinite loop that can break with a value.Functions & Closures
Function definition
function add(a: number, b: number): number {
return a + b;
}
console.log(add(2, 3)); fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
println!("{}", add(2, 3));
} Functions use
fn, annotate every parameter, and declare the return type after ->. The last expression is returned implicitly when it has no trailing semicolon, so the explicit return is usually omitted.Closures
const makeAdder = (n: number) => (x: number) => x + n;
const addFive = makeAdder(5);
console.log(addFive(10)); fn main() {
let make_adder = |n: i32| move |x: i32| x + n;
let add_five = make_adder(5);
println!("{}", add_five(10));
} Closures are written with
|args| bars. The move keyword forces the inner closure to take ownership of n so it can outlive the call — a concern that does not exist in TypeScript, where captured variables are simply shared by reference.Structs & Methods
Structs
interface Point { x: number; y: number; }
const point: Point = { x: 3, y: 4 };
console.log(point.x, point.y); struct Point {
x: f64,
y: f64,
}
fn main() {
let point = Point { x: 3.0, y: 4.0 };
println!("{} {}", point.x, point.y);
} A
struct is a named record type with fixed, typed fields — like a TypeScript interface, but a concrete value with a known memory layout. Fields are private to the module unless marked pub.Methods via impl
class Point {
constructor(public x: number, public y: number) {}
distance(): number { return Math.hypot(this.x, this.y); }
}
console.log(new Point(3, 4).distance()); struct Point {
x: f64,
y: f64,
}
impl Point {
fn distance(&self) -> f64 {
self.x.hypot(self.y)
}
}
fn main() {
let point = Point { x: 3.0, y: 4.0 };
println!("{}", point.distance());
} Methods live in a separate
impl block, not inside the struct. The first parameter &self is the borrowed receiver (the Rust equivalent of this); &mut self or self change whether the method borrows or consumes the value.Enums & Option
Enums with data
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; w: number; h: number };
function area(shape: Shape): number {
return shape.kind === "circle"
? Math.PI * shape.radius ** 2
: shape.w * shape.h;
}
console.log(area({ kind: "rect", w: 3, h: 4 })); enum Shape {
Circle(f64),
Rect(f64, f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
Shape::Rect(width, height) => width * height,
}
}
fn main() {
println!("{}", area(&Shape::Rect(3.0, 4.0)));
} Rust enums are true tagged unions — each variant can carry its own data, exactly like a TypeScript discriminated union but built into the language. The compiler checks that a
match handles every variant, so adding a case is caught at compile time.Option instead of null
function firstEven(numbers: number[]): number | undefined {
return numbers.find(n => n % 2 === 0);
}
const found = firstEven([1, 3, 4]);
console.log(found ?? "none"); fn first_even(numbers: &[i32]) -> Option<i32> {
numbers.iter().copied().find(|n| n % 2 == 0)
}
fn main() {
match first_even(&[1, 3, 4]) {
Some(value) => println!("{value}"),
None => println!("none"),
}
} Rust has no
null. Absence is modeled by Option<T>, an enum of Some(value) or None, so the type system forces you to handle the empty case. This is TypeScript's T | undefined made mandatory and explicit.Traits
Traits
interface Speaker { speak(): string; }
class Dog implements Speaker {
speak(): string { return "Woof"; }
}
function announce(s: Speaker): string { return s.speak(); }
console.log(announce(new Dog())); trait Speaker {
fn speak(&self) -> String;
}
struct Dog;
impl Speaker for Dog {
fn speak(&self) -> String {
String::from("Woof")
}
}
fn announce(speaker: &impl Speaker) -> String {
speaker.speak()
}
fn main() {
println!("{}", announce(&Dog));
} A
trait is Rust's interface: a set of method signatures a type can implement with impl Trait for Type. Unlike TypeScript's structural interfaces, trait implementation is explicit and can be added to types you do not own.Deriving traits
interface Point { x: number; y: number; }
const point: Point = { x: 1, y: 2 };
console.log(JSON.stringify(point)); #[derive(Debug, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 1, y: 2 };
println!("{point:?}");
} The
#[derive(...)] attribute auto-implements common traits at compile time — Debug gives the {:?} formatting, Clone gives an explicit .clone(). It is boilerplate you would hand-write in TypeScript, generated for free here.Error Handling
Result instead of throw
function parseDouble(text: string): number {
const value = Number(text);
if (Number.isNaN(value)) throw new Error("not a number");
return value * 2;
}
try {
console.log(parseDouble("21"));
} catch (error) {
console.log((error as Error).message);
} fn parse_double(text: &str) -> Result<i32, std::num::ParseIntError> {
let value: i32 = text.parse()?;
Ok(value * 2)
}
fn main() {
match parse_double("21") {
Ok(value) => println!("{value}"),
Err(error) => println!("{error}"),
}
} Recoverable failures are returned, not thrown: a function yields
Result<T, E> — either Ok(value) or Err(error). The caller must handle both arms, so an error can never be silently ignored the way an uncaught exception can.The ? operator
function totalLength(parts: string[]): number {
let total = 0;
for (const part of parts) total += part.length;
return total;
}
console.log(totalLength(["ab", "cde"])); fn doubled(text: &str) -> Result<i32, std::num::ParseIntError> {
let value: i32 = text.parse()?; // ? returns early on Err
Ok(value * 2)
}
fn main() {
println!("{:?}", doubled("21"));
println!("{:?}", doubled("oops"));
} The
? operator is Rust's ergonomic shortcut: on an Ok it unwraps the value, and on an Err it returns that error from the enclosing function immediately. It turns verbose match-and-return chains into the linear flow you would write with exceptions, but explicitly.Pattern Matching
match expressions
function describe(n: number): string {
switch (true) {
case n === 0: return "zero";
case n > 0: return "positive";
default: return "negative";
}
}
console.log(describe(-5)); fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
n if n > 0 => "positive",
_ => "negative",
}
}
fn main() {
println!("{}", describe(-5));
} match is an expression that must be exhaustive — the compiler rejects it unless every possibility is covered, with _ as the catch-all. A if guard on an arm adds a condition, giving far more power than a switch.Destructuring in patterns
const point = { x: 1, y: 2 };
const { x, y } = point;
console.log(x, y); struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 1, y: 2 };
let Point { x, y } = point;
println!("{x} {y}");
} Patterns work in
let too, so destructuring a struct mirrors TypeScript object destructuring. Combined with match, the same syntax can bind fields, match literals, and branch — one unified mechanism rather than several.Generics
Generic functions
function largest<T>(items: T[], greater: (a: T, b: T) => boolean): T {
let best = items[0];
for (const item of items) if (greater(item, best)) best = item;
return best;
}
console.log(largest([3, 9, 2], (a, b) => a > b)); fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut best = items[0];
for &item in items {
if item > best {
best = item;
}
}
best
}
fn main() {
println!("{}", largest(&[3, 9, 2]));
} Generic parameters go in
<T> like TypeScript, but each comes with trait bounds — T: PartialOrd + Copy means "any type that can be ordered and copied." The bounds tell the compiler exactly which operations the body may use, so generic code is checked once, not per instantiation.