What are tuple structs

Tuple structs are structs whose fields are accessed by index instead of name, written with parentheses. Great for the newtype pattern and for compact two-field types where positions are obvious.

A struct with no field names

You're modeling a 2D point. The natural thing in Rust is:

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

That works. But for something this small, naming x and y feels almost ceremonial. You'd rather just write Point(3.0, 4.0) and move on. Rust gives you a way to do exactly that. It's called a tuple struct.

A tuple struct is a struct whose fields don't have names. They're indexed by position, like a tuple, but they have a real type name. So Point(3.0, 4.0) is a Point, not just (f64, f64). The type system treats it as distinct, and that distinction is most of the reason tuple structs exist.

Defining one

The syntax mirrors a tuple. Parentheses, comma-separated types, no field names.

// A 2D point with two f64 components.
struct Point(f64, f64);

// An RGB color, three u8 channels.
struct Color(u8, u8, u8);

// A single-field tuple struct, sometimes called a "newtype" (more on this later).
struct UserId(u64);

There's no : between names and types, because there are no names. Just types. The fields are accessed by their position: .0, .1, .2, and so on, in declaration order.

fn main() {
    // Construct like a function call. The struct name is the constructor.
    let origin = Point(0.0, 0.0);
    let red = Color(255, 0, 0);

    // Read fields by index.
    println!("x = {}", origin.0);
    println!("red = {}, {}, {}", red.0, red.1, red.2);
}

If that looks weird, that's because it is a bit weird. You're not used to seeing .0 in production code. But once you've used tuple structs a few times, the index access stops feeling odd. It's a tradeoff: less typing in exchange for slightly less readable field access.

Why not just use a tuple

This is the question that trips people up. If Point(0.0, 0.0) looks like (0.0, 0.0), why bother with the struct at all?

The answer is type safety. A tuple (f64, f64) is the same type whether it's a point, a velocity vector, a complex number, or two random unrelated numbers. The compiler can't help you if you mix them up. A Point(3.0, 4.0) and a Velocity(3.0, 4.0) look identical at runtime but are distinct types. You can't pass one where the other is expected.

struct Point(f64, f64);
struct Velocity(f64, f64);

// Takes a Point. The compiler will reject a Velocity here.
fn distance_from_origin(p: Point) -> f64 {
    (p.0 * p.0 + p.1 * p.1).sqrt()
}

fn main() {
    let p = Point(3.0, 4.0);
    let v = Velocity(3.0, 4.0);

    println!("{}", distance_from_origin(p));      // ok
    // println!("{}", distance_from_origin(v));   // E0308: mismatched types
}

Try that last line and you get something like:

error[E0308]: mismatched types
  --> src/main.rs:13:34
   |
13 |     println!("{}", distance_from_origin(v));
   |                    -------------------- ^ expected `Point`, found `Velocity`
   |                    |
   |                    arguments to this function are incorrect

That's the whole point. The struct name is a label that the compiler enforces. Two values with the same shape but different meaning are now different types.

The newtype pattern

A tuple struct with exactly one field shows up so often it has its own name: the newtype pattern. The classic example is wrapping a primitive to give it meaning.

// A user ID is "really" just a u64, but we want the type system to know
// it's not a generic number you can multiply or compare to a price.
struct UserId(u64);
struct OrderId(u64);

fn cancel_order(id: OrderId) {
    // ... the OrderId is only allowed here, not just any u64.
}

fn main() {
    let user = UserId(42);
    let order = OrderId(42);

    // cancel_order(user);   // E0308: expected OrderId, found UserId
    cancel_order(order);     // ok
}

You'd be amazed how many bugs this pattern prevents. Anywhere you've got two integers that mean different things (user id vs order id, byte count vs row count, milliseconds vs seconds), wrapping one of them in a tuple struct turns "oops I passed the wrong number" from a runtime mystery into a compile error.

Destructuring

Tuple structs play nicely with pattern matching. You can pull the fields out by position the same way you would with a tuple.

struct Point(f64, f64);

fn describe(p: Point) {
    // Bind the two fields to local names. The struct name is part of the pattern.
    let Point(x, y) = p;
    println!("({}, {})", x, y);
}

The let Point(x, y) = p is a destructuring let. Useful in match arms too:

enum Shape {
    Circle(f64),                    // a circle with radius
    Rectangle(f64, f64),            // a rectangle with width and height
}

fn area(s: Shape) -> f64 {
    // Each arm names the variant and binds the inner fields by position.
    match s {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h) => w * h,
    }
}

Notice how enum variants with payloads use the same syntax as tuple structs. That's not a coincidence: each variant of an enum behaves a lot like a tuple struct in its own right.

When tuple structs hurt readability

There's a sweet spot. Tuple structs work great for one or two fields with obvious meaning (Point(x, y), UserId(42)). They start to hurt around three. By the time you have Address("123 Main", "Springfield", "IL", "62701"), nobody reading the call site can tell which string is the city. A regular struct with named fields is clearer there.

A rough guide:

  • One field: prefer a tuple struct (newtype pattern).
  • Two fields, where the order is universally understood (x/y, key/value, lo/hi): tuple struct is fine.
  • Three or more fields, or any ambiguity about which is which: use a regular struct with named fields.

You can also mix: define a tuple struct, then add inherent methods that give the fields names through accessors:

struct Point(f64, f64);

impl Point {
    fn x(&self) -> f64 { self.0 }    // accessor for the first field
    fn y(&self) -> f64 { self.1 }    // accessor for the second field
}

That gives you the compact Point(3.0, 4.0) constructor while letting consumers write p.x() and p.y() when reading.

Visibility of fields

Tuple struct fields, like named fields, follow the module visibility rules. Without pub, they're private to the module. If you want consumers to read .0 from outside, you need pub on the field.

pub struct Wrapper(pub i32);    // both the type and its field are pub

pub struct Sealed(i32);         // the type is pub, the field is private

The Sealed form is useful when you want to expose a type but force callers to use a constructor function, so you can validate inputs or change the internal representation later without breaking anything outside the module.

Where to go next