Course contentsShow
← Rust
Lesson 764 of 1,9106. Generics and Static DispatchFree lesson

The Newtype Pattern with Generics

Wrap primitive types in generic containers to enforce domain-specific type safety.

In this lesson, you will learn how to use the Newtype pattern combined with generics to enforce domain-specific type safety while keeping your code flexible.

The Problem: Primitive Obsession

In complex systems, we often pass around primitive types like f64 or i32 to represent different units, such as "meters" vs. "seconds," or "Celsius" vs. "Fahrenheit." If your functions accept raw f64 values, it is dangerously easy to accidentally pass a "temperature" value into a function expecting a "distance" value. The compiler won't complain because, to Rust, they are both just floating-point numbers.

The Generic Newtype Solution

The Newtype pattern solves this by wrapping a primitive inside a tuple struct. When we add generics, we gain the ability to create highly reusable "containers" that enforce these boundaries without writing repetitive code for every single unit type.

Consider this example:

struct Measurement<T, U>(T, std::marker::PhantomData<U>);

struct Meters;
struct Seconds;

fn main() {
    let distance = Measurement::<f64, Meters>(10.0, std::marker::PhantomData);
    let time = Measurement::<f64, Seconds>(2.0, std::marker::PhantomData);

    // This prevents logical errors:
    // add_distance_to_time(distance, time); // Compile error!
}

By wrapping the value in a generic struct, you create a distinct type. Even though both Measurement instances hold an f64 under the hood, the compiler treats Measurement<f64, Meters> and Measurement<f64, Seconds> as completely different entities.

The PhantomData<U> is a clever trick: it tells the compiler that our struct "acts as if" it owns a U, even though U (the unit type) takes up no space at runtime. This creates a zero-cost abstraction that enforces strict architectural boundaries. You can now define methods specifically for Measurement<T, Meters> that wouldn't make sense for Measurement<T, Seconds>, effectively moving logical domain constraints into the type system where they can be verified at compile time.

Key Takeaway: Use generic Newtypes to distinguish between semantically different values that share the same underlying representation, ensuring that domain logic errors are caught before your code even runs.