Home Design Patterns Newtype

Newtype

Ownership and Lifetimes

Wrap a value in a distinct type so the compiler stops treating inches and degrees as interchangeable.

Purpose

A newtype is a struct with exactly one field, existing for no reason other than to be a different type from the thing it contains. Inches(i32) and Degrees(i32) hold the same bits and occupy the same space, and the compiler will refuse to let you pass one where the other is expected.

It costs nothing at runtime. The wrapper has the same memory layout as the value inside it and the calls compile away entirely, which is what makes the technique viable in places where a wrapper object carrying a vtable pointer would be unthinkable.

There is a second job it does in Rust that has nothing to do with units. You may only implement a trait for a type if you own one or the other. Wrapping a foreign type in your own newtype makes it yours, and the restriction lifts. A large fraction of newtypes in real Rust code exist purely for that reason.

Design

Declare a tuple struct with one field. Derive the traits that make sense for the wrapped value and deliberately omit the ones that do not. This is the part people rush: the value of Inches comes as much from what it refuses to do as from what it permits. Adding two lengths is meaningful, so implement addition. Multiplying two lengths is not, so do not.

UML class diagram: Inches and Degrees each wrap a single i32 but are distinct types, and ArmController accepts only the correct one in each method.
Both wrap the same primitive. The diagram is almost content-free, which is the honest depiction: nothing happens here at runtime, and the entire effect is on what the compiler will accept.

Example

The Adapter page on this site contains a small example that has bothered me since I wrote it. The old Autobot arm was commanded in inches of travel, the new one in degrees of rotation, and the adapter converts between them:

public void moveVertical(int inch) {
    adaptee.rotateXaxis(convert.x(inch));
}
The C# original. Both quantities are int, so nothing prevents a caller passing degrees to a method expecting inches.

Every quantity in that system is an int. Inches, degrees, milliseconds, part counts, station numbers. The conversion is correct, but nothing stops a caller getting it wrong at the call site, and if they do, the program runs and the arm moves to the wrong place. On a machine with a fixture in its work envelope, that is not an abstract concern.

With newtypes, that mistake stops being possible to write.

#[derive(Copy, Clone, PartialEq, PartialOrd)]
struct Inches(i32);

#[derive(Copy, Clone, PartialEq, PartialOrd)]
struct Degrees(i32);

impl Inches {
    fn to_degrees(self, counts_per_inch: i32) -> Degrees {
        Degrees(self.0 * counts_per_inch)
    }
}

fn move_vertical(distance: Inches) {
    // some code
}

fn rotate_x(angle: Degrees) {
    // some code
}

fn main() {
    let travel = Inches(12);

    move_vertical(travel);                 // fine
    rotate_x(travel.to_degrees(400));      // fine, conversion is explicit

    // rotate_x(travel);
    // error: expected Degrees, found Inches
}
The conversion still exists and is still the only place the arithmetic lives. What changed is that skipping it is now a compile error rather than a wrong movement.

Seen in the Wild

Rust's standard library is full of them: Duration wraps a time span so it cannot be confused with a bare count of seconds, NonZeroUsize encodes a guarantee in the type, and Wrapping<T> exists solely to select different arithmetic behavior. The name comes from Haskell, where newtype is a language keyword doing exactly this. F# takes it furthest with units of measure checked by the compiler. The canonical cautionary tale is the Mars Climate Orbiter, lost in 1999 because ground software produced impulse values in pound-force seconds while the spacecraft expected newton-seconds, a mismatch that a type system would have caught and a code review did not.

Comparisons

  • Marker Interface also adds type information with no behavior, but it tags an existing type with a capability. A newtype creates a genuinely new type that the original cannot be substituted for.
  • Adapter wraps something to change its interface at runtime. A newtype wraps something to change nothing at runtime and everything at compile time.
  • Private Class Data is a common motivation. Wrapping a value and exposing only the operations that make sense for it is information hiding at the smallest possible scale.
  • Typestate is the same instinct scaled up from one value to a whole object's lifecycle.

Why It Exists

My take

Newtype exists because primitive types carry no meaning, and every system of any size eventually contains two quantities of the same primitive type that must never be confused. The bug it prevents is not exotic. It is passing the right-shaped value from the wrong slot, and it is invisible in review precisely because the types agree.

What makes the pattern worth naming is the price. Making illegal states unrepresentable is an old and widely agreed-upon goal, and the usual objection is that it costs performance or ceremony. A newtype costs neither: identical layout, identical generated code, one line of declaration. It is close to the cheapest safety you can buy anywhere in software, which makes the fact that most languages cannot express it a genuine indictment of those languages.

I would have taken this over most of the GoF catalog on the ERP system. That database dealt in cycle times in tenths of a second, durations in seconds, employee IDs, machine IDs, and station numbers, and every one of them was an integer in the schema and an int in the client. The bugs that survived longest were never architectural. They were a number that was correct in every respect except which column it had come from.

Criticisms

The boilerplate is real and the language does not help as much as it should. Every trait the inner type had, the wrapper has to opt back into, so a newtype around a string that wants to be printed, compared, hashed, and serialized needs all of that spelled out. Derives cover the common cases and the rest is manual. For a quantity used in three places this is straightforwardly not worth it.

Access through .0 is noisy, and the noise pushes people toward implementing Deref so the wrapper transparently behaves like the thing it wraps. That is widely considered a mistake, because it quietly restores the substitutability the newtype existed to prevent, and it produces confusing method resolution. If you find yourself wanting Deref, you probably wanted a type alias, which gives you the convenience and none of the safety.

Applied without judgment it makes signatures worse rather than better. Not every integer needs a name. The test I use is whether two values of the same primitive type could plausibly be swapped at a call site without anything noticing, and whether the consequences of that would be worth a class. For inches and degrees on a robot arm, clearly yes. For a loop counter, no.