Rust Structs: Named Fields and Tuple Structs

Rust provides two primary struct variants: named field structs and tuple structs. This isn't arbitrary complexity—each serves distinct purposes in building type-safe, maintainable systems. Named...

Key Insights

  • Named field structs provide clarity and self-documenting code, while tuple structs offer lightweight wrappers when field names add no semantic value
  • The newtype pattern using tuple structs creates type-safe abstractions with zero runtime cost, preventing bugs like mixing up kilometers and miles
  • Both struct types support methods and associated functions equally through impl blocks, so choose based on API clarity rather than capability limitations

Understanding Rust’s Struct Variants

Rust provides two primary struct variants: named field structs and tuple structs. This isn’t arbitrary complexity—each serves distinct purposes in building type-safe, maintainable systems. Named field structs excel when your data structure benefits from explicit field labels. Tuple structs shine when creating lightweight wrappers or when positional semantics are obvious.

The choice between them impacts code readability, refactoring ease, and API design. Let’s examine both with practical examples.

// Named field struct
struct User {
    username: String,
    email: String,
    active: bool,
}

// Tuple struct
struct Color(u8, u8, u8);

Both define custom types, but they communicate intent differently. The User struct clearly labels each piece of data. The Color struct relies on positional understanding—everyone knows RGB values come in red-green-blue order.

Named Field Structs: The Default Choice

Named field structs are Rust’s equivalent to records or objects in other languages. Each field has an explicit name and type, making the structure self-documenting.

struct Point {
    x: f64,
    y: f64,
}

// Creating instances
let origin = Point { x: 0.0, y: 0.0 };
let point = Point { x: 3.5, y: 7.2 };

// Field access
println!("X coordinate: {}", point.x);

Rust provides field initialization shorthand when variable names match field names:

fn create_user(username: String, email: String) -> User {
    User {
        username,  // Shorthand for username: username
        email,     // Shorthand for email: email
        active: true,
    }
}

This shorthand reduces boilerplate without sacrificing clarity. The field names remain visible in the struct definition.

Modifying fields requires mutable bindings:

let mut point = Point { x: 0.0, y: 0.0 };
point.x = 5.0;
point.y = 10.0;

// Note: Rust doesn't support field-level mutability
// The entire binding must be mutable

Named field structs work well for configuration objects, domain models, and any data where field names add meaningful documentation:

struct DatabaseConfig {
    host: String,
    port: u16,
    username: String,
    password: String,
    max_connections: u32,
}

Anyone reading this code understands exactly what each field represents without consulting documentation.

Tuple Structs: Lightweight Wrappers

Tuple structs combine struct naming with tuple-like field access. They’re ideal when you need a distinct type but field names would be redundant or obvious from context.

struct Color(u8, u8, u8);
struct Point3D(f64, f64, f64);

let red = Color(255, 0, 0);
let origin = Point3D(0.0, 0.0, 0.0);

// Access via indexing
println!("Red channel: {}", red.0);
println!("X coordinate: {}", origin.0);

The killer use case for tuple structs is the newtype pattern—wrapping a single value to create a distinct type:

struct Kilometers(i32);
struct Miles(i32);

fn distance_to_destination() -> Kilometers {
    Kilometers(500)
}

// This won't compile - type safety prevents mixing units
fn calculate_fuel(distance: Miles) -> f64 {
    distance.0 as f64 * 0.05
}

let dist = distance_to_destination();
// calculate_fuel(dist); // Error: expected Miles, found Kilometers

This pattern prevents entire classes of bugs at compile time with zero runtime overhead. The wrapper type exists only during compilation—at runtime, Kilometers is just an i32.

Other common newtype uses include:

struct UserId(u64);
struct OrderId(u64);
struct ProductId(u64);

// Prevents accidentally passing wrong ID type
fn get_user(id: UserId) -> Option<User> {
    // Implementation
}

Without these wrappers, all IDs would be plain u64 values, and the compiler couldn’t catch mistakes like get_user(order_id).

Choosing Between Named and Tuple Structs

The decision comes down to clarity and intent. Here’s the same concept implemented both ways:

// Named field version
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

// Tuple struct version
struct Rect(u32, u32);

impl Rect {
    fn area(&self) -> u32 {
        self.0 * self.1
    }
}

The named version is clearer. When you see rect.width, you immediately understand what you’re accessing. With rect.0, you need to remember which dimension comes first.

However, tuple structs win for obvious positional data:

struct RGB(u8, u8, u8);
struct Coordinate(f64, f64);

// Clear enough without labels
let red = RGB(255, 0, 0);
let point = Coordinate(10.5, 20.3);

Consider refactoring implications. Adding a field to a named struct is straightforward:

struct User {
    username: String,
    email: String,
    active: bool,
    created_at: DateTime<Utc>,  // New field added
}

Adding to a tuple struct changes field indices:

struct Color(u8, u8, u8);
// Later: struct Color(u8, u8, u8, u8);  // Added alpha channel
// Now all .3 accesses need updating to .4

Methods Work Identically for Both

Both struct types support methods and associated functions through impl blocks:

struct Point {
    x: f64,
    y: f64,
}

impl Point {
    // Associated function (constructor)
    fn new(x: f64, y: f64) -> Self {
        Point { x, y }
    }
    
    // Method
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

struct Color(u8, u8, u8);

impl Color {
    fn new(r: u8, g: u8, b: u8) -> Self {
        Color(r, g, b)
    }
    
    fn red(&self) -> u8 {
        self.0
    }
    
    fn to_hex(&self) -> String {
        format!("#{:02X}{:02X}{:02X}", self.0, self.1, self.2)
    }
}

let point = Point::new(3.0, 4.0);
let color = Color::new(255, 128, 0);

Methods can compensate for tuple struct field access limitations. Instead of exposing raw indices, provide named accessor methods like color.red().

Practical Patterns in Real Applications

Combine both struct types strategically. Use named structs for complex types and tuple structs for simple wrappers:

struct UserId(u64);
struct Email(String);

struct UserProfile {
    id: UserId,
    email: Email,
    display_name: String,
    created_at: i64,
}

impl UserProfile {
    fn new(id: u64, email: String, display_name: String) -> Self {
        UserProfile {
            id: UserId(id),
            email: Email(email),
            display_name,
            created_at: current_timestamp(),
        }
    }
    
    fn email_address(&self) -> &str {
        &self.email.0
    }
}

This design provides type safety for IDs and emails while keeping the main structure readable.

Another pattern: tuple structs for units and measurements:

struct Meters(f64);
struct Seconds(f64);
struct MetersPerSecond(f64);

impl Meters {
    fn per(&self, time: Seconds) -> MetersPerSecond {
        MetersPerSecond(self.0 / time.0)
    }
}

let distance = Meters(100.0);
let time = Seconds(9.58);
let speed = distance.per(time);

This creates a domain-specific language for physics calculations with compile-time unit checking.

Making the Right Choice

Use named field structs when:

  • Fields have non-obvious meanings
  • You’re building domain models or configuration objects
  • The struct will evolve and gain new fields
  • Code reviewers unfamiliar with the codebase need to understand it quickly

Use tuple structs when:

  • Implementing the newtype pattern for type safety
  • Field positions have obvious semantic meaning (RGB, coordinates)
  • You’re wrapping a single value
  • The struct is simple and unlikely to grow

Don’t overthink it. Start with named fields for anything complex. Reach for tuple structs when you need lightweight type distinctions. Both are first-class citizens in Rust—the compiler treats them equally, so your choice is purely about expressing intent to human readers.

The best Rust code uses both judiciously, letting each variant play to its strengths while building type-safe, self-documenting systems.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.