10.4Tuple structs and unit structs

Last updated June 13, 2026

Named-field structs are the workhorses, but Rust has two leaner variants for cases where named fields are more ceremony than the data deserves. Both are still program-defined types; both still create a brand-new type the compiler will keep separate from everything else.

Tuple structs

A tuple struct is a struct whose fields have positions instead of names. You give the type a name, then list the field types in parentheses:

struct Rgb(u8, u8, u8);

fn main() {
    let orange = Rgb(206, 66, 43);
    println!("R={} G={} B={}", orange.0, orange.1, orange.2);
}
R=206 G=66 B=43

It looks like a tuple (lesson 4.11) and you reach into it the same way, with .0, .1, .2. The difference is the name in front. Rgb(206, 66, 43) is not a plain (u8, u8, u8) tuple; it is a value of the distinct type Rgb. A function that takes an Rgb will refuse a bare tuple, and vice versa.

Use a tuple struct when the fields' meaning is obvious from order and context, so names would just be noise. A color as (red, green, blue) is the classic case: everyone agrees on the order, and Rgb(206, 66, 43) reads better than a named-field struct with three one-letter fields. When the order isn't obvious, prefer named fields. Nobody should have to remember whether .2 was the balance or the year.

The newtype pattern

The most valuable use of a single-field tuple struct has its own name: the newtype pattern. You wrap an existing type to give it a fresh identity the compiler enforces.

Consider a program that handles both meters and feet, both stored as f64. Nothing stops you from adding a meters value to a feet value; they're both f64, so the compiler is happy and your bridge is wrong. Wrap each in its own type and the mistake becomes a compile error:

struct Meters(f64);
struct Feet(f64);

fn describe(distance: Meters) {
    println!("{} meters", distance.0);
}

fn main() {
    let height = Meters(8848.0);
    describe(height);
    // describe(Feet(29029.0)); // refused: Feet is not Meters
}
8848 meters

Meters and Feet are both "an f64 underneath," but they are different types, so describe(Feet(29029.0)) won't compile. This is the lesson 10.1 idea ("EmailAddress beats String when precision earns its keep") made mechanical. The wrapper costs nothing at runtime; it's the same f64 in memory, with a type-level label that catches mix-ups for free.

Key insight

A newtype turns a comment into a compiler check. Instead of writing // this f64 is in meters and hoping every caller read it, you make Meters a type, and "this is meters, not feet" becomes a rule the compiler enforces at every call site. The same trick wraps a String into a UserId, a u64 into a Timestamp, and so on, whenever you want to stop two look-alike values from being mistaken for each other.

Unit structs

The leanest variant has no fields at all. A unit struct is declared with just a name and a semicolon:

struct AlwaysReady;

fn main() {
    let _marker = AlwaysReady;
}

It's called a unit struct because it holds no data, the way the unit type () from lesson 1.11 holds no data. At first this seems pointless: a type with nothing in it stores nothing, so why create one? The answer is that a type can carry behavior and meaning without carrying data. You'll meet the real use in chapter 16, where you can attach an implementation of some shared behavior (a trait) to a unit struct, so the type itself, with no fields, becomes a thing you can pass around and dispatch on. For now, just recognize the syntax: a bare name and a semicolon is a valid struct.

For advanced readers

Unit structs come into their own as zero-sized markers: a Celsius and a Fahrenheit unit struct used as type parameters, or a state-machine state that needs no data. Because they occupy no memory, the compiler can represent a whole collection of them for free. Chapter 16's traits and chapter 17's generics are where this stops being a curiosity.

Three flavors, one idea

All three are structs; they differ only in how the fields are addressed:

Reach for named fields unless you have a specific reason not to. The other two are tools for specific jobs, not everyday alternatives.

Quiz time

Question #1

Define a tuple struct Point3d holding three f64 values, create the point (1.0, 2.0, 3.0), and print its middle coordinate.

Show solution
struct Point3d(f64, f64, f64);

fn main() {
    let p = Point3d(1.0, 2.0, 3.0);
    println!("{}", p.1);
}

Prints 2. Fields are positional, so the middle one is .1.

Question #2

Why won't this compile, and what real bug does the refusal prevent?

struct Meters(f64);
struct Feet(f64);

fn main() {
    let total = Meters(100.0).0 + Feet(50.0).0;
    println!("{total}");
}
Show solution

Trick question: that line actually does compile, because .0 unwraps each to a bare f64 and adding two f64s is fine, giving 150. The newtype only protects you while values stay wrapped. The bug the pattern prevents is passing a Feet where a function expects Meters: describe(Feet(50.0)) is refused. Once you manually unwrap with .0, you've taken responsibility, the same way as did in lesson 4.10.

Question #3

What's the difference between struct Empty; and struct Empty {}? When would you write a unit struct at all?

Show solution

Both are fieldless types; struct Empty; is the unit-struct form (no braces, semicolon) and is the conventional way to write "no fields." You'd use one when you need a type that carries behavior or meaning but no data, which becomes useful with traits in chapter 16. Until then it's enough to recognize the syntax.

Structs can now hold data in three shapes. The next lesson attaches behavior to them: methods, the impl block, and the three forms of self that turn chapters 8 and 9 into everyday syntax.