Structs, Enums & Traits

Rust structs, enums with data, trait definitions and implementations, and common patterns

Rust has no classes. Instead, you define data with structs and enums, and behavior with traits and impl blocks. Composition replaces inheritance.

Structs

// Named fields (most common)
struct User {
    name: String,
    age: u32,
    active: bool,
}

let user = User {
    name: String::from("Ada"),
    age: 30,
    active: true,
};

// Access fields
println!("{}", user.name);

// Mutable struct to modify fields
let mut user = User { name: String::from("Ada"), age: 30, active: true };
user.age = 31;

// Struct update syntax (like JS spread)
let user2 = User { name: String::from("Grace"), ..user };
// user2 inherits age and active from user
// WARNING: user.name is now MOVED — cannot use user.name anymore

// Tuple struct (named tuple)
struct Point(i32, i32);
let p = Point(3, 4);
println!("x={}, y={}", p.0, p.1);

// Unit struct (no fields — marker type)
struct Initialized;

Methods with impl

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // Associated function (like a static method — no &self)
    fn new(width: u32, height: u32) -> Self {
        Self { width, height }
    }

    fn square(size: u32) -> Self {
        Self { width: size, height: size }
    }

    // Method — takes &self (immutable borrow)
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }

    // Mutable method — takes &mut self
    fn double(&mut self) {
        self.width *= 2;
        self.height *= 2;
    }

    // Consuming method — takes self (ownership)
    fn into_parts(self) -> (u32, u32) {
        (self.width, self.height)
    }
}

// Usage
let rect = Rectangle::new(10, 20);
println!("area: {}", rect.area());

let mut square = Rectangle::square(5);
square.double();

Derive macros

Automatically implement common traits:

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone();
println!("{:?}", p1);      // Debug: Point { x: 1, y: 2 }
assert_eq!(p1, p2);        // PartialEq

Common derive targets: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default.

Enums

Enums in Rust can carry data — they’re sum types (algebraic data types), not just named constants.

// Simple enum (like TypeScript string enum)
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

// Enum with data
enum Shape {
    Circle(f64),                    // tuple variant
    Rectangle { width: f64, height: f64 },  // struct variant
    Triangle(f64, f64, f64),        // tuple variant with 3 fields
}

// Enum with different data per variant
enum Message {
    Quit,                           // no data
    Move { x: i32, y: i32 },       // struct variant
    Write(String),                  // tuple variant
    ChangeColor(u8, u8, u8),       // tuple variant
}

// Pattern matching
fn process(msg: Message) {
    match msg {
        Message::Quit => println!("quit"),
        Message::Move { x, y } => println!("move to ({}, {})", x, y),
        Message::Write(text) => println!("write: {}", text),
        Message::ChangeColor(r, g, b) => println!("color: ({}, {}, {})", r, g, b),
    }
}

// if let for single variant
if let Message::Write(text) = msg {
    println!("{}", text);
}

Option and Result are enums

// Option is just an enum
enum Option<T> {
    Some(T),
    None,
}

// Result is just an enum
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Enums with methods

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String),  // state on quarter
}

impl Coin {
    fn value(&self) -> u32 {
        match self {
            Coin::Penny => 1,
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter(state) => {
                println!("State quarter from {}", state);
                25
            }
        }
    }
}

Traits

Traits define shared behavior — similar to TypeScript interfaces but with implementations.

// Define a trait
trait Summary {
    fn summarize(&self) -> String;

    // Default implementation
    fn preview(&self) -> String {
        format!("{}...", self.summarize().chars().take(20).collect::<String>())
    }
}

// Implement for a type
struct Article {
    title: String,
    content: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}: {}", self.title, self.content)
    }
    // preview gets the default implementation
}

let article = Article {
    title: "Rust".to_string(),
    content: "A systems programming language".to_string(),
};
println!("{}", article.summarize());
println!("{}", article.preview());

Trait as parameter

// impl Trait syntax (simpler)
fn print_summary(item: &impl Summary) {
    println!("{}", item.summarize());
}

// Trait bound syntax (more explicit, needed for multiple bounds)
fn print_summary<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

// Multiple trait bounds
fn display<T: Summary + std::fmt::Display>(item: &T) {
    println!("{}{}", item, item.summarize());
}

// where clause (cleaner for complex bounds)
fn process<T, U>(a: &T, b: &U) -> String
where
    T: Summary + Clone,
    U: Summary,
{
    format!("{} + {}", a.summarize(), b.summarize())
}

Common standard library traits

// Debug — for {:?} printing
#[derive(Debug)]
struct Point { x: i32, y: i32 }
println!("{:?}", point);

// Clone — for deep copying
#[derive(Clone)]
struct Data { values: Vec<i32> }
let d2 = d1.clone();

// Copy — for implicit copy (stack types only)
#[derive(Copy, Clone)]
struct Coord { x: i32, y: i32 }
let c2 = c1; // copied, not moved

// Default
#[derive(Default)]
struct Config {
    host: String,   // defaults to ""
    port: u16,      // defaults to 0
    verbose: bool,  // defaults to false
}
let config = Config::default();
let custom = Config { port: 8080, ..Config::default() };

// Display — for {} printing
impl std::fmt::Display for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

// From / Into — type conversion
impl From<i32> for Score {
    fn from(value: i32) -> Self {
        Score { points: value }
    }
}
let score: Score = Score::from(100);
let score: Score = 100.into(); // also works

Associated types in traits

trait Container {
    type Item;  // associated type

    fn get(&self, index: usize) -> Option<&Self::Item>;
    fn len(&self) -> usize;
}

struct Stack<T> {
    items: Vec<T>,
}

impl<T> Container for Stack<T> {
    type Item = T;

    fn get(&self, index: usize) -> Option<&Self::Item> {
        self.items.get(index)
    }

    fn len(&self) -> usize {
        self.items.len()
    }
}

Trait objects (dynamic dispatch)

When you need runtime polymorphism (like a TypeScript interface):

// dyn Trait — heap-allocated, dynamic dispatch
fn print_all(items: Vec<Box<dyn Summary>>) {
    for item in items {
        println!("{}", item.summarize());
    }
}

// Or with references
fn print_summaries(items: &[&dyn Summary]) {
    for item in items {
        println!("{}", item.summarize());
    }
}

let article = Article { /* ... */ };
let tweet = Tweet { /* ... */ };
print_all(vec![Box::new(article), Box::new(tweet)]);

Note: Trait objects (dyn Trait) use dynamic dispatch (vtable lookup at runtime). Generic bounds (impl Trait / <T: Trait>) use static dispatch (monomorphization at compile time). Prefer generics for performance; use trait objects when you need heterogeneous collections.