Rust for
TypeScript Developers
A practical guide to Rust from zero, written for developers who already know TypeScript. Every concept is shown side-by-side — what you know, and its Rust equivalent.
Why Learn Rust?
Rust gives you the performance of C/C++ with the safety of a modern language — and no garbage collector. It's increasingly used for CLI tools, WebAssembly, systems programming, and high-performance backends.
| Feature | TypeScript | Rust |
|---|---|---|
| Runtime | Node.js / V8 (JIT + GC) | Native binary (no runtime) |
| Memory management | Garbage collected | Ownership (compile-time) |
| Type system | Structural, optional strict | Nominal, always strict |
| Null safety | null / undefined (strict mode helps) |
Option<T> — no null |
| Error handling | try/catch exceptions |
Result<T,E> — explicit |
| Concurrency | Single-threaded event loop | Multi-threaded, data-race-free |
| Package manager | npm / pnpm / yarn | Cargo |
| Config file | package.json |
Cargo.toml |
Setup & Tooling
Installing Rust is simple via rustup — Rust's version manager, analogous to nvm.
# Install rustup (like nvm for Node)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify
rustc --version # Rust compiler
cargo --version # Package manager + build tool
cargo new hello-rust # creates a new binary project
cargo new mylib --lib # creates a library project
cd hello-rust
cargo run # compile + run
cargo build # compile only
cargo build --release # optimized build
cargo check # type-check without compiling (fast)
cargo test # run tests
cargo add serde # add a dependency (like npm install)
Project Structure
my-project/
├── package.json # deps + scripts
├── tsconfig.json
└── src/
└── index.ts
my-project/
├── Cargo.toml # deps + metadata
├── Cargo.lock # lockfile
└── src/
└── main.rs # entry point
[package]
name = "hello-rust"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
Variables & Types
Rust variables are immutable by default — the opposite of TypeScript's let.
You have to explicitly opt-in to mutation with mut.
// mutable by default
let x = 5;
x = 10; // ok
// immutable
const y = 5;
// y = 10; // error
// explicit type
let count: number = 0;
// immutable by default
let x = 5;
// x = 10; // ERROR!
// mutable
let mut y = 5;
y = 10; // ok
// explicit type
let count: i32 = 0;
Primitive Types
| TypeScript | Rust | Notes |
|---|---|---|
number |
i8, i16, i32, i64, i128 |
Signed integers |
number |
u8, u16, u32, u64, u128 |
Unsigned integers |
number |
f32, f64 |
Floating point |
boolean |
bool |
true / false |
string |
&str, String |
Two string types (explained later) |
[T, T, T] tuple |
(T, T, T) |
Fixed-size, heterogeneous |
T[] array |
[T; N] |
Fixed-size array |
T[] (dynamic) |
Vec<T> |
Growable vector (like JS Array) |
void |
() (unit type) |
Empty return |
never |
! |
Diverging (never returns) |
let x = 42; // inferred as i32
let y = 3.14; // inferred as f64
let flag = true; // bool
// Constants (like TypeScript `const` at module level)
const MAX_POINTS: u32 = 100_000; // underscores for readability
// Tuples
let point: (i32, i32) = (10, 20);
let (px, py) = point; // destructuring
println!("{}", point.0); // tuple index access
// Fixed arrays
let arr: [i32; 3] = [1, 2, 3];
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
Shadowing
Rust allows shadowing — redeclaring a variable with let in the same scope. This is different from TypeScript where you can't redeclare with let.
let x = 5;
let x = x + 1; // shadows previous x = 6
let x = x * 2; // shadows again x = 12
// Can even change type when shadowing!
let spaces = " "; // &str
let spaces = spaces.len(); // now usize — totally valid
Strings
Rust has two string types, which confuses most beginners. Think of &str as a read-only view
and String as a mutable, owned buffer.
const name: string = "Alice";
let greeting = `Hello, ${name}!`;
greeting += " How are you?";
let name: &str = "Alice"; // string slice (static)
let mut greeting = String::from("Hello, ");
greeting.push_str(name);
greeting.push('!'); // single char
// format! = template literal
let msg = format!("Hello, {}!", name);
&str | String | |
|---|---|---|
| Size | Fixed (known at compile time) | Dynamic (heap allocated) |
| Mutability | Immutable view | Mutable, owned |
| Where | Stack / static memory | Heap |
| Analogy | const s = "hi" | let s = new String("hi") |
| Convert to other | s.to_string() | &s or s.as_str() |
&str for function parameters (it accepts both). Use String when you need to own or modify the string data.let s = String::from("Hello, World!");
// Length
s.len(); // 13
// Contains / starts_with / ends_with
s.contains("World"); // true
s.starts_with("Hello"); // true
// Split and collect
let words: Vec<&str> = s.split(', ').collect();
// Replace
let new_s = s.replace("World", "Rust");
// Trim, uppercase, lowercase
" hello ".trim(); // "hello"
"hello".to_uppercase(); // "HELLO"
// Parse string to number
let n: i32 = "42".parse().unwrap();
// Number to string
let s = 42.to_string();
Functions
Functions in Rust use fn instead of function. Parameter and return types are required — no implicit any.
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
const double = (x: number): number => x * 2;
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = return value
}
// Closure (lambda)
let double = |x: i32| x * 2;
()). This is a common source of bugs for beginners!
// Basic function
fn greet(name: &str) -> String {
format!("Hello, {}!", name) // no semicolon = returned
}
// Multiple return values via tuple
fn min_max(nums: &[i32]) -> (i32, i32) {
let min = *nums.iter().min().unwrap();
let max = *nums.iter().max().unwrap();
(min, max)
}
let (lo, hi) = min_max(&[3, 1, 4, 1, 5]);
// No return value (unit type)
fn print_hello() { // -> () is implicit
println!("Hello!");
}
// Early return
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
return 0.0; // explicit early return
}
a / b
}
Control Flow
if and loop are similar to TypeScript, but Rust also has match —
a supercharged version of switch that's exhaustive and can destructure values.
const x = 5;
if (x > 0) {
console.log("positive");
} else if (x < 0) {
console.log("negative");
} else {
console.log("zero");
}
// Ternary
const label = x > 0 ? "pos" : "neg";
let x = 5;
if x > 0 { // no parentheses!
println!("positive");
} else if x < 0 {
println!("negative");
} else {
println!("zero");
}
// if is an expression (no ternary needed)
let label = if x > 0 { "pos" } else { "neg" };
Loops
// for...of
for (const n of [1, 2, 3]) {
console.log(n);
}
// while
let i = 0;
while (i < 5) { i++; }
// range-like
for (let i = 0; i < 5; i++) { }
// for...in
for n in [1, 2, 3] {
println!("{}", n);
}
// while
let mut i = 0;
while i < 5 { i += 1; } // no i++
// range (0..5 = exclusive, 0..=5 = inclusive)
for i in 0..5 { println!("{}", i); }
// loop = infinite loop (use break to exit)
let result = loop {
break 42; // loop can return a value!
};
match — Rust's supercharged switch
switch (day) {
case "Mon":
console.log("Start");
break;
case "Fri":
console.log("End");
break;
default:
console.log("Midweek");
}
match day {
"Mon" => println!("Start"),
"Fri" => println!("End"),
_ => println!("Midweek"), // _ = default
}
// match is an expression
let label = match score {
90..=100 => "A", // ranges!
80..=89 => "B",
_ => "C",
};
Structs
Structs are Rust's version of TypeScript interfaces and classes combined.
They define data shape. Methods are added separately in an impl block.
interface User {
name: string;
age: number;
active: boolean;
}
class User {
constructor(
public name: string,
public age: number
) {}
greet() {
return `Hi, I'm ${this.name}`;
}
}
const u = new User("Alice", 30);
struct User {
name: String,
age: u32,
active: bool,
}
impl User {
// constructor pattern
fn new(name: &str, age: u32) -> Self {
User { name: name.to_string(),
age, active: true }
}
// method (&self = read-only)
fn greet(&self) -> String {
format!("Hi, I'm {}", self.name)
}
}
let u = User::new("Alice", 30);
// Create instance (all fields required)
let user1 = User {
name: String::from("Alice"),
age: 30,
active: true,
};
// Struct update syntax (like JS spread)
let user2 = User {
name: String::from("Bob"),
..user1 // copy remaining fields from user1
};
// Destructuring
let User { name, age, .. } = user2;
// Tuple structs (named tuples)
struct Point(f64, f64);
let p = Point(1.0, 2.0);
println!("{}", p.0); // 1.0
// impl methods: &self = read, &mut self = write, self = consume
impl User {
fn deactivate(&mut self) {
self.active = false;
}
}
Enums
Rust enums are far more powerful than TypeScript's. Each variant can carry different data — similar to TypeScript discriminated unions but built into the language and exhaustively checked.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; w: number; h: number }
| { kind: "triangle" };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "rect": return s.w * s.h;
default: return 0;
}
}
enum Shape {
Circle { radius: f64 },
Rect { w: f64, h: f64 },
Triangle,
}
fn area(s: Shape) -> f64 {
match s {
Shape::Circle { radius } =>
std::f64::consts::PI * radius * radius,
Shape::Rect { w, h } => w * h,
Shape::Triangle => 0.0,
} // exhaustive — compiler enforces all cases
}
let s1 = Shape::Circle { radius: 5.0 };
let s2 = Shape::Rect { w: 3.0, h: 4.0 };
let s3 = Shape::Triangle;
println!("Area: {}", area(s1)); // ~78.5
// Enum with impl
impl Shape {
fn is_round(&self) -> bool {
matches!(self, Shape::Circle { .. })
}
}
Option<T> — No More Null
Rust has no null or undefined. Instead, values that might be absent are
wrapped in Option<T> — an enum with two variants: Some(value) or None.
function findUser(id: number): User | null {
return db.find(u => u.id === id) ?? null;
}
const user = findUser(1);
if (user !== null) {
console.log(user.name);
}
// Optional chaining
const name = user?.name ?? "unknown";
fn find_user(id: u32) -> Option<User> {
db.iter().find(|u| u.id == id).cloned()
}
let user = find_user(1);
if let Some(u) = user {
println!("{}", u.name);
}
// unwrap_or = ?? "fallback"
let name = find_user(1)
.map(|u| u.name)
.unwrap_or(String::from("unknown"));
Working with Option
let maybe: Option<i32> = Some(42);
let nothing: Option<i32> = None;
// unwrap (panics if None — use only when certain)
maybe.unwrap(); // 42
// unwrap_or (like ?? in TypeScript)
nothing.unwrap_or(0); // 0
nothing.unwrap_or_else(|| expensive_default());
// map (transform the inner value if Some)
maybe.map(|x| x * 2); // Some(84)
// and_then (flatMap — chain Options)
maybe.and_then(|x| if x > 0 { Some(x) } else { None });
// is_some / is_none
maybe.is_some(); // true
nothing.is_none(); // true
// match pattern
match maybe {
Some(v) => println!("Got: {}", v),
None => println!("Nothing"),
}
// if let (concise single-branch match)
if let Some(v) = maybe {
println!("Got: {}", v);
}
Result<T, E> — Explicit Errors
Rust replaces exceptions with Result<T, E> — Ok(value) for success,
Err(error) for failure. Errors become part of the type signature, so you can't accidentally ignore them.
async function readFile(path: string): Promise<string> {
try {
return await fs.readFile(path, "utf8");
} catch (err) {
throw new Error(`Failed: ${err}`);
}
}
use std::fs;
use std::io;
fn read_file(path: &str) -> Result<String, io::Error> {
let content = fs::read_to_string(path)?; // ? = propagate error
Ok(content)
}
? operator is Rust's equivalent of await unwrapping — it extracts the
Ok value or immediately returns the Err to the caller. It's how you
propagate errors upward without nesting.
let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> = Err(String::from("oops"));
// unwrap (panics on Err)
ok.unwrap(); // 42
// unwrap_or / unwrap_or_else
err.unwrap_or(0); // 0
// map / map_err (transform Ok or Err side)
ok.map(|x| x * 2); // Ok(84)
// match pattern
match ok {
Ok(v) => println!("Value: {}", v),
Err(e) => println!("Error: {}", e),
}
// ? operator — propagate error to caller
fn do_work() -> Result<(), String> {
let val = risky_operation()?; // returns Err early if fails
println!("Got: {}", val);
Ok(())
}
Ownership
Ownership is Rust's most unique concept — and the hardest for newcomers. It's the system that allows Rust to manage memory safely without a garbage collector. There's no equivalent in TypeScript.
- Each value has exactly one owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (freed).
// Stack types (Copy trait) — just copied, no ownership transfer
let x: i32 = 5;
let y = x; // x is copied
println!("{}", x); // still valid!
// Heap types (String, Vec, etc.) — ownership moves
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED into s2
// println!("{}", s1); // COMPILE ERROR: s1 no longer owns the data
println!("{}", s2); // s2 is the owner now
// To keep both, explicitly clone (deep copy)
let s3 = s2.clone(); // both s2 and s3 are valid
println!("{} {}", s2, s3);
fn take_ownership(s: String) -> String {
println!("{}", s);
s // return ownership back to caller
}
let s = String::from("hello");
let s = take_ownership(s); // ownership moved in AND returned
println!("{}", s); // valid again
// Better: pass a reference instead (next section!)
Borrowing & References
Instead of transferring ownership, you can borrow a value with & (read-only) or
&mut (mutable). Borrowing lets you use a value without taking ownership.
function printLength(s: string): void {
console.log(s.length);
// caller still owns s
}
const s = "hello";
printLength(s);
console.log(s); // still accessible
fn print_length(s: &String) { // & = borrow
println!("{}", s.len());
// s dropped here, but not the data
}
let s = String::from("hello");
print_length(&s); // pass a reference
println!("{}", s); // still valid!
let mut s = String::from("hello");
// RULE 1: Many immutable references at once is OK
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2); // fine
// RULE 2: Only ONE mutable reference at a time
let r3 = &mut s;
// let r4 = &mut s; // COMPILE ERROR: can't have two &mut
r3.push_str(" world");
// RULE 3: Can't mix mutable + immutable references
let r5 = &s;
// let r6 = &mut s; // COMPILE ERROR while r5 exists
println!("{}", r5);
// Mutable reference after immutable references are done
let r7 = &mut s; // OK now — r5 no longer used
r7.push_str("!");
&) and write locks (&mut).
Many readers at once is fine. A writer requires exclusive access. The compiler enforces this at compile time — no runtime deadlocks possible.
Collections
Rust's standard collections: Vec<T> (like JS Array), HashMap<K,V> (like JS Map/Object),
and HashSet<T> (like JS Set).
const nums: number[] = [1, 2, 3];
nums.push(4);
nums[0]; // 1
nums.len; // .length
const doubled = nums.map(x => x * 2);
const evens = nums.filter(x => x % 2 === 0);
const sum = nums.reduce((a, b) => a + b, 0);
let mut nums: Vec<i32> = vec![1, 2, 3];
nums.push(4);
nums[0]; // 1
nums.len(); // method call
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();
let evens: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();
let sum: i32 = nums.iter().sum();
const map = new Map<string, number>();
map.set("a", 1);
map.get("a"); // 1 | undefined
map.has("a"); // true
map.delete("a");
use std::collections::HashMap;
let mut map: HashMap<String, i32> = HashMap::new();
map.insert(String::from("a"), 1);
map.get("a"); // Option<&i32>
map.contains_key("a"); // true
map.remove("a");
// Vec — common patterns
let mut v = vec![3, 1, 4, 1, 5];
v.sort(); // in-place sort
v.dedup(); // remove consecutive duplicates
v.retain(|&x| x > 2); // keep elements matching predicate
v.iter().enumerate() // like Array.entries()
.for_each(|(i, val)| println!("{}: {}", i, val));
// HashMap — or_insert pattern (great for counting)
let mut counts: HashMap<char, u32> = HashMap::new();
for c in "hello".chars() {
let count = counts.entry(c).or_insert(0);
*count += 1;
}
// counts = {'h':1, 'e':1, 'l':2, 'o':1}
Closures & Iterators
Rust closures use |args| syntax (pipe characters instead of arrow). Iterators are lazy —
chaining .map().filter() doesn't allocate until you .collect().
const nums = [1, 2, 3, 4, 5];
const result = nums
.filter(x => x % 2 === 0)
.map(x => x * x)
.reduce((acc, x) => acc + x, 0);
// result = 20
let nums = vec![1, 2, 3, 4, 5];
let result: i32 = nums.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.sum();
// result = 20
let threshold = 10; // captured from outer scope
// Captures by reference (borrows threshold)
let filter = |x: &i32| *x > threshold;
// move closure — takes ownership of captured vars
let add_n = move |x: i32| x + threshold;
// threshold still usable since i32 is Copy
// Iterator methods
let v = vec![1, 2, 3];
v.iter().any(|&x| x > 2); // true (like .some())
v.iter().all(|&x| x > 0); // true (like .every())
v.iter().find(|&&x| x == 2); // Some(&2)
v.iter().position(|&x| x == 2); // Some(1)
v.iter().count(); // 3
v.iter().max(); // Some(&3)
v.iter().min(); // Some(&1)
v.iter().rev(); // reversed iterator
v.iter().zip(["a", "b", "c"]); // like zip()
v.iter().flat_map(|&x| 0..x); // flatMap
v.iter().take(2); // first 2
v.iter().skip(1); // skip first
Traits
Traits are Rust's equivalent of TypeScript interfaces — but they define behavior (methods) rather than shape. You can implement a trait for any type, even types you didn't create.
interface Greetable {
greet(): string;
}
class Dog implements Greetable {
greet() { return "Woof!"; }
}
function sayHi(g: Greetable) {
console.log(g.greet());
}
trait Greetable {
fn greet(&self) -> String;
}
struct Dog;
impl Greetable for Dog {
fn greet(&self) -> String { "Woof!".into() }
}
fn say_hi(g: &impl Greetable) {
println!("{}", g.greet());
}
// #[derive] auto-implements common traits
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
let p = Point { x: 1.0, y: 2.0 };
// Debug — like console.log(JSON.stringify(p))
println!("{:?}", p); // Point { x: 1.0, y: 2.0 }
println!("{:#?}", p); // pretty-printed
// Clone — explicit deep copy
let p2 = p.clone();
// PartialEq — enables ==
let same = p == p2; // true
| TypeScript | Rust trait | Purpose |
|---|---|---|
Auto JSON.stringify | Debug | Format for debugging |
... spread | Clone | Explicit deep copy |
== | PartialEq | Equality comparison |
< > | PartialOrd | Ordering |
.toString() | Display | User-facing formatting |
| Iterable protocol | Iterator | Custom iteration |
| — | Default | Zero-value constructor |
Modules & Cargo
Rust uses a module system similar to ES modules, but everything is private by default.
Use pub to expose things, and use to bring them into scope.
// math.ts
export function add(a: number, b: number) {
return a + b;
}
// main.ts
import { add } from "./math";
console.log(add(1, 2));
// src/math.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// src/main.rs
mod math; // declare module
use math::add; // bring into scope
println!("{}", add(1, 2));
// Inline module
mod utils {
pub fn helper() -> &'static str { "help" }
fn private() {} // private by default
}
// Use paths
use std::collections::HashMap; // std library
use utils::helper;
// Multiple imports
use std::collections::{HashMap, HashSet, BTreeMap};
// Rename
use std::io::Result as IoResult;
// Re-export
pub use self::utils::helper;
// File structure for larger projects:
// src/
// main.rs ← binary entry
// lib.rs ← library root
// auth/
// mod.rs ← declares the auth module
// login.rs
// session.rs
Popular Crates (packages)
| Need | Crate | npm equivalent |
|---|---|---|
| Async runtime | tokio | Node.js built-in |
| HTTP client | reqwest | axios / fetch |
| HTTP server | axum, actix-web | express / fastify |
| JSON | serde_json | JSON.parse/stringify |
| Serialize | serde | zod / class-transformer |
| CLI args | clap | commander / yargs |
| Logging | tracing | winston / pino |
| Error handling | anyhow, thiserror | — |
| Database (ORM) | sqlx, diesel | prisma / drizzle |
| Date/time | chrono | date-fns / luxon |
| UUID | uuid | uuid |
| Regex | regex | Built-in RegExp |
Quick Cheatsheet
TypeScript → Rust at a glance.
// Variables
let x = 5 → let x = 5; // immutable
let x = 5; x = 6 → let mut x = 5; x = 6;
const X = 5 → const X: i32 = 5; // needs type
// Functions
(a: number): number → (a: i32) -> i32
x => x * 2 → |x| x * 2
// Types
number → i32 / f64
string → String / &str
boolean → bool
T[] → Vec<T>
[T, U] → (T, U)
Map<K, V> → HashMap<K, V>
T | null → Option<T>
Promise<T> → async fn -> T
// Null handling
x ?? "default" → x.unwrap_or("default")
x?.method() → x.map(|v| v.method())
x! → x.unwrap()
// Error handling
try { } catch(e) { } → match result { Ok(v) => .., Err(e) => .. }
throw new Error("msg") → return Err("msg".into())
await fn() → fn()? // propagate error
// Control flow
if (x > 0) → if x > 0 // no parens
switch (x) { } → match x { }
x > 0 ? "y" : "n" → if x > 0 { "y" } else { "n" }
for (const v of arr) → for v in arr
for (let i=0;ifor i in 0..n
// Printing
console.log(x) → println!("{}", x)
console.log(obj) → println!("{:?}", obj) // needs Debug
`Hello ${name}` → format!("Hello {}", name)
// Struct/class
interface Foo { x: T } → struct Foo { x: T }
class Foo { method() } → impl Foo { fn method(&self) }
implements Interface → impl Trait for Type
{ ...obj, x: 1 } → Struct { x: 1, ..obj }
// Imports
import { x } from "m" → use m::x;
export function foo → pub fn foo
npm install pkg → cargo add pkg
Next Steps
Now that you understand the basics, here's where to go next.
Beginner Projects to Build
- CLI tool — use
clapfor args, read/write files. Great first project. - Simple HTTP server — build a REST API with
axum+serde_json. - Word counter — practice HashMap, file reading, iterators.
- Mini grep — implement a simplified
grepto learn string processing. - Todo app CLI — serialization with
serde, file persistence.
Topics to Learn Next
- Lifetimes — when the borrow checker needs explicit hints (
'a). - Generics — write functions that work for any type:
fn foo<T: Display>(x: T) - Async / Tokio —
async fnand.awaitfor non-blocking I/O. - Error handling —
thiserrorfor library errors,anyhowfor apps. - Smart pointers —
Box<T>,Rc<T>,Arc<T>,RefCell<T>. - WebAssembly — compile Rust to WASM and call it from TypeScript!
Resources
- The Book —
doc.rust-lang.org/book— the official free Rust book. Read chapters 1-10 first. - Rustlings — small exercises to practice each concept (
cargo install rustlings). - Rust by Example —
doc.rust-lang.org/rust-by-example - docs.rs — documentation for every crate on crates.io.
- crates.io — the npm registry of Rust.