- Published on
Rust for TypeScript Developers
- Authors
- Name
- Manuel Sousa
- @mlrcbsousa
The following are notes from the Frontend Masters - Rust for TypeScript Developers course by ThePrimeagen. I’ve adapted key ideas and added insights from my own experience.
This post covers the basics of Rust from a TypeScript developer’s perspective, including common Rust constructs, memory concepts, and practical tips for building efficient applications.
Introduction to Rust for TypeScript Developers
This course emphasizes Rust’s advantages for handling memory and its nuanced control over application performance, with topics like vectors, iterators, enums, traits, and the basics of memory management.
Why Rust?
Rust offers a balance between performance and code safety, making it ideal for projects requiring fine-grained control over memory. Here are a few core contrasts with TypeScript:
- Mutability by Default: Rust defaults to immutability, where you explicitly specify mutable variables with
mut
. - Explicit Error Handling: Errors are managed through
Result
andOption
enums, avoiding runtime surprises from undefined or null values. - Fine-Grained Memory Control: You directly manage memory, which can optimize runtime performance significantly.
Rust Basics
Variables and Shadowing
In Rust, variables are immutable by default, meaning their values cannot be changed unless marked as mutable:
let foo = 5; // Immutable
let mut bar = 5; // Mutable
bar = 6;
Rust also supports shadowing, allowing you to reuse variable names while changing their types or values, which isn’t possible in TypeScript.
let foo = 42; // `foo` is an integer
let foo = "hello"; // `foo` is now a string
If Statements and Loops
Rust syntax for conditionals is similar to TypeScript, but without parentheses around conditions.
if condition {
// do something
} else {
// do something else
}
Rust’s loop structures are flexible, with for
, while
, and loop
.
for i in 0..10 {
println!("i is {}", i);
}
Working with Collections
Vectors (Vec<T>
) are Rust’s dynamic arrays and are similar to TypeScript arrays but require explicit memory management.
let mut nums = vec![1, 2, 3];
nums.push(4); // [1, 2, 3, 4]
Functions and Closures
Rust functions specify parameter types and return types, a key difference from TypeScript’s more flexible typing.
fn add(a: i32, b: i32) -> i32 {
a + b
}
Closures are similar in both languages, but Rust enforces stricter type-checking.
let add_one = |x: i32| x + 1;
Traits: Rust's Equivalent of TypeScript Interfaces
Traits define shared behavior and are similar to interfaces in TypeScript, though they cannot define properties directly. Here’s an example trait for an Area
method:
trait Area {
fn area(&self) -> f64;
}
Implementing Area
for a Rectangle
struct:
struct Rectangle {
width: f64,
height: f64,
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
Enums: The Power of Pattern Matching
Rust’s enums are more powerful than TypeScript’s because they can hold data, making pattern matching very versatile.
enum Color {
Red,
Green,
Blue,
}
fn print_color(color: Color) {
match color {
Color::Red => println!("Red"),
Color::Green => println!("Green"),
Color::Blue => println!("Blue"),
}
}
Memory Management in Rust
Stack vs. Heap
Rust emphasizes memory control, helping you understand and optimize where data is stored.
- Stack: Stores fixed-size variables, offering faster access.
- Heap: Used for dynamic memory, such as
Vec
orString
, requiring explicit memory management.
Rust encourages stack allocations but allows heap usage with Box
, Vec
, and other structures.
Borrowing and Ownership
Rust’s borrow checker enforces strict memory rules, avoiding data races and unexpected behaviors. Here are three core rules to remember:
- There can only be one owner of a value.
- Multiple immutable borrows (references) are allowed if there are no mutable references.
- A mutable borrow is exclusive.
Example of Borrowing
let s = String::from("hello");
let r1 = &s; // Immutable borrow
let r2 = &s; // Another immutable borrow
println!("{} and {}", r1, r2);
Error Handling
Rust does not use exceptions like TypeScript. Instead, it relies on Result
and Option
enums, making error handling explicit and enforced at compile time.
Example with Result
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Cannot divide by zero"))
} else {
Ok(a / b)
}
}
Iterators and Collect
Rust’s iterator pattern is a central part of the language, allowing efficient collection transformations without creating intermediate arrays.
let vec = vec![1, 2, 3];
let result: Vec<i32> = vec.iter().map(|x| x + 1).collect();
println!("{:?}", result); // Outputs: [2, 3, 4]
Building Structs and Implementing Traits
To group data, Rust uses structs, which can have methods attached through trait implementations.
struct Circle {
radius: f64,
}
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
Working with Option
and Result
Rust provides these enums for safe handling of null values and errors, replacing undefined
or null
in TypeScript.
Using Option
fn find_user(id: u32) -> Option<String> {
Some(String::from("User"))
}
Using Result
fn parse_number(input: &str) -> Result<i32, std::num::ParseIntError> {
input.parse()
}
Conclusion and Resources
Rust offers TypeScript developers a way to write high-performance applications with strong memory and error control. Its unique approach to ownership, borrowing, and enums brings benefits for stability and performance.
This post covered the fundamentals of Rust and how they compare to TypeScript, but there is much more to learn. For in-depth exploration, refer to the resources below.