Home Design Patterns Newtype
Newtype
Ownership and LifetimesWrap 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.
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));
}
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
}
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.