Structs

Tuple Structs

A tuple struct is a hybrid between a named struct and a tuple: the struct itself has a distinct type name, while its fields are unnamed and accessed by positional index (.0, .1, .2) or pattern destructuring.

Declaring and Using Tuple Structs

A tuple struct is declared with the struct keyword, a type name in UpperCamelCase, a parenthesized list of field types, and a required trailing semicolon (;):

#[derive(Debug, Clone, Copy, PartialEq)]
struct RgbColor(u8, u8, u8);

#[derive(Debug, Clone, Copy, PartialEq)]
struct Point3D(f64, f64, f64);

fn main() {
    let coral = RgbColor(255, 127, 80);
    let origin = Point3D(0.0, 0.0, 0.0);

    // 1. Access fields by 0-based positional index (`.0`, `.1`, `.2`):
    println!("Coral RGB: R={}, G={}, B={}", coral.0, coral.1, coral.2);

    // 2. Or unpack fields by destructuring in `let`:
    let Point3D(x, y, z) = origin;
    println!("Origin: ({x}, {y}, {z})");
}
Coral RGB: R=255, G=127, B=80
Origin: (0, 0, 0)

Even if two tuple structs have the exact same inner field types—such as struct Point3D(i32, i32, i32); and struct Color(i32, i32, i32);—they are completely distinct, incompatible types in the type system! Unlike plain tuples (i32, i32, i32), you can never accidentally pass a Color to a function expecting a Point3D.

The Newtype Pattern (struct Wrapper(Inner);)

The single most important use of tuple structs in Rust is the Newtype Pattern: wrapping a single field inside a 1-element tuple struct (struct Meters(f64); or struct UserId(u64);).

The Newtype Pattern gives you three capabilities with zero runtime memory or CPU overhead:

1. Compile-Time Unit and Domain Safety

#[derive(Debug, Clone, Copy)]
struct Meters(f64);

#[derive(Debug, Clone, Copy)]
struct Seconds(f64);

#[derive(Debug, Clone, Copy)]
struct MetersPerSecond(f64);

fn velocity(d: Meters, t: Seconds) -> MetersPerSecond {
    MetersPerSecond(d.0 / t.0)
}

fn main() {
    let distance = Meters(400.0);
    let duration = Seconds(43.03);

    // Passing `velocity(duration, distance)` in the wrong order is a compile-time error!
    let speed = velocity(distance, duration);
    println!("Speed: {:.2} m/s", speed.0);
}
Speed: 9.30 m/s

2. Implementing External Traits on External Types (Bypassing the Orphan Rule)

Rust's Orphan Rule states that you can only implement a trait for a type (impl Trait for Type) if at least one of Trait or Type is defined in your own crate. For example, you cannot implement std::fmt::Display directly on Vec<String> because both Display and Vec belong to std.

Wrapping Vec<String> in a local tuple struct struct PathList(Vec<String>); creates a local type that you can implement Display (or any external trait) on freely:

use std::fmt;

struct PathList(Vec<String>);

impl fmt::Display for PathList {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "[{}]", self.0.join(" -> "))
    }
}

fn main() {
    let route = PathList(vec![
        String::from("home"),
        String::from("developer"),
        String::from("rust"),
    ]);

    println!("Route: {route}");
}
Route: [home -> developer -> rust]

3. Hiding Representation Details via Field Visibility

Just like named-field structs, the positional fields of a tuple struct are private by default!

  • pub struct UserId(u64); makes the type UserId public while keeping its inner u64 field private (so external modules cannot forge an unvalidated UserId(0) or read .0 directly).
  • pub struct UserId(pub u64); makes both the type and its .0 field public.

Tuple Struct Constructor Syntax Is Also a Function

Declaring struct Kilometers(u32); automatically defines a constructor function Kilometers: fn(u32) -> Kilometers, which can be passed directly to .map():

#[derive(Debug)]
struct Kilometers(u32);

fn main() {
    let raw_distances = [5, 10, 42];
    let typed: [Kilometers; 3] = raw_distances.map(Kilometers);

    println!("{typed:?}");
}
[Kilometers(5), Kilometers(10), Kilometers(42)]