Published on

Rust for TypeScript Developers

Authors

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 and Option 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:

rust
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.

rust
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.

rust
if condition {
    // do something
} else {
    // do something else
}

Rust’s loop structures are flexible, with for, while, and loop.

rust
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.

rust
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.

rust
fn add(a: i32, b: i32) -> i32 {
    a + b
}

Closures are similar in both languages, but Rust enforces stricter type-checking.

rust
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:

rust
trait Area {
    fn area(&self) -> f64;
}

Implementing Area for a Rectangle struct:

rust
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.

rust
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 or String, 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:

  1. There can only be one owner of a value.
  2. Multiple immutable borrows (references) are allowed if there are no mutable references.
  3. A mutable borrow is exclusive.

Example of Borrowing

rust
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

rust
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.

rust
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.

rust
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

rust
fn find_user(id: u32) -> Option<String> {
    Some(String::from("User"))
}

Using Result

rust
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.


Resources