Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

NewType Pattern

The NewType pattern isn't one of the classic Gang of Four design patterns, but it's a fundamental Rust idiom that provides type safety and semantic clarity. It's particularly powerful in Rust due to the language's zero-cost abstractions and strong type system.

What is the NewType Pattern?

The NewType pattern involves wrapping an existing type in a new struct to create a distinct type with the same underlying representation but different semantics. This provides:

  • Type Safety: Prevents mixing up semantically different values of the same underlying type
  • API Control: You can implement only the traits and methods you want to expose
  • Zero Runtime Cost: The wrapper is eliminated at compile time
  • Clear Intent: Makes code more self-documenting

Comparison with Java

In Java, you might use wrapper classes, but they have runtime overhead:

// Java - has runtime overhead
public class UserId {
    private final int value;

    public UserId(int value) {
        this.value = value;
    }

    public int getValue() { return value; }
}

In Rust, the NewType pattern has zero runtime cost due to the compiler's optimizations.

Rust Implementation

use std::fmt;

// NewType pattern - wrapping primitive types for type safety
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(u32);

#[derive(Debug, Clone, PartialEq)]
pub struct Email(String);

// Implementing methods for NewType
impl UserId {
    pub fn new(id: u32) -> Self {
        UserId(id)
    }

    pub fn as_u32(&self) -> u32 {
        self.0
    }
}


impl Email {
    pub fn new(email: String) -> Result<Self, &'static str> {
        if email.contains('@') {
            Ok(Email(email))
        } else {
            Err("Invalid email format")
        }
    }

    pub fn domain(&self) -> Option<&str> {
        self.0.split('@').nth(1)
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

// Custom Display implementation
impl fmt::Display for UserId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "User#{}", self.0)
    }
}


impl fmt::Display for Email {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

// Example usage demonstrating type safety
pub struct User {
    id: UserId,
    email: Email,
}


impl User {
    pub fn new(id: u32, email: String) -> Result<Self, &'static str> {
        Ok(User {
            id: UserId::new(id),
            email: Email::new(email)?,
        })
    }

    pub fn id(&self) -> UserId {
        self.id
    }

    pub fn email(&self) -> &Email {
        &self.email
    }
}



fn main() {
    // Create some NewType instances
    let user_id = UserId::new(123);
    let email = Email::new("user@example.com".to_string()).unwrap();

    println!("User ID: {}", user_id);
    println!("Email domain: {:?}", email.domain());

    // Create user
    let user = User::new(123, "alice@example.com".to_string()).unwrap();

    // Demonstrate that NewTypes prevent accidental mixing
    let another_user_id = UserId::new(789);

    println!("User IDs can be compared: {}", user_id == another_user_id);
    println!("User ID as u32: {}", user_id.as_u32());
}

More Complex NewType Example

A more sophisticated example showing validation and behavior:

use std::fmt;

// Example: A more complex NewType with validation and behavior
#[derive(Debug, Clone, PartialEq)]
pub struct Temperature(f64);

impl Temperature {
    pub fn celsius(temp: f64) -> Self {
        Temperature(temp)
    }

    pub fn fahrenheit(temp: f64) -> Self {
        Temperature((temp - 32.0) * 5.0 / 9.0)
    }

    pub fn to_celsius(&self) -> f64 {
        self.0
    }

    pub fn to_fahrenheit(&self) -> f64 {
        self.0 * 9.0 / 5.0 + 32.0
    }

    pub fn to_kelvin(&self) -> f64 {
        self.0 + 273.15
    }
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}°C", self.0)
    }
}

fn main() {
    // Temperature example
    let temp_c = Temperature::celsius(25.0);
    let temp_f = Temperature::fahrenheit(77.0);

    println!("Temperature in Celsius: {}", temp_c);
    println!("Temperature in Fahrenheit: {:.1}°F", temp_c.to_fahrenheit());
    println!("Temperature in Kelvin: {:.1}K", temp_c.to_kelvin());
}

Key Benefits Demonstrated

  1. Type Safety: UserId types can't be accidentally swapped with other wrapped types, even though they both wrap u32

  2. Zero Cost: The wrapper types are compiled away - no runtime overhead

  3. Controlled API: You decide which operations are available (notice we don't derive Add for IDs)

  4. Validation: The Email type enforces basic validation at construction time

  5. Semantic Clarity: Temperature::celsius(25.0) is much clearer than just 25.0

Advanced NewType Patterns

You can also implement traits selectively:

#![allow(unused)]
fn main() {
// Only implement specific traits you want
impl std::ops::Add for Temperature {
    type Output = Temperature;

    fn add(self, other: Temperature) -> Temperature {
        Temperature(self.0 + other.0)
    }
}
}

Relation to Gang of Four

While not a GoF pattern, NewType often works with other patterns:

  • Facade: NewTypes can simplify complex APIs
  • Adapter: Wrap foreign types to match your interface
  • Decorator: Add behavior to existing types without inheritance

The NewType pattern is particularly powerful in Rust because it leverages the type system for safety without runtime cost, something that's harder to achieve in classical OOP languages like Java.


Core Rust concepts introduced

1. Tuple Structs and the NewType Pattern

#![allow(unused)]
fn main() {
pub struct UserId(u32);
}
  • Tuple structs: Single-field structs with unnamed fields
  • NewType pattern: Wrapping existing types for type safety
  • Field access: Use .0 to access the wrapped value
  • Zero-cost abstraction: Wrapper is eliminated at compile time

2. Ownership and Borrowing

#![allow(unused)]
fn main() {
pub fn as_str(&self) -> &str {
    &self.0  // Borrowing the inner string data
}
}
  • Ownership: Every value has exactly one owner
  • Borrowing: Using & to create references without taking ownership
  • &self: Immutable borrow of the struct instance
  • &str: String slice (borrowed view of string data)
  • Memory safety: Prevents use-after-free and data races at compile time

3. String Types

#![allow(unused)]
fn main() {
pub struct Email(String);     // Owned string
pub fn as_str(&self) -> &str  // String slice (borrowed view)
}
  • String: Owned, heap-allocated, growable string
  • &str: Immutable string slice (view into string data)
  • Automatic conversion: String can be borrowed as &str
  • Memory efficiency: Pass &str instead of String when you don't need ownership

4. Derive Macros

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(u32);
}
  • Automatic trait implementations: Compiler generates code for common traits
  • Debug: Enables {:?} and {:#?} formatting for debugging
  • Clone: Explicit copying with .clone() method
  • Copy: Implicit copying for simple stack-allocated types
  • PartialEq/Eq: Equality comparisons with == and !=
  • Hash: Enables use in HashMap, HashSet, etc.

5. Result Type and Error Handling

#![allow(unused)]
fn main() {
pub fn new(email: String) -> Result<Self, &'static str> {
    if email.contains('@') {
        Ok(Email(email))
    } else {
        Err("Invalid email format")
    }
}
}
  • Result<T, E>: Rust's approach to fallible operations
  • No exceptions: Errors are values, not thrown exceptions
  • Explicit handling: Must handle both Ok(value) and Err(error) cases
  • Type safety: Prevents forgotten error handling at compile time

6. Trait Implementation

#![allow(unused)]
fn main() {
impl fmt::Display for UserId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "User#{}", self.0)
    }
}
}
  • Traits: Similar to interfaces, define shared behavior
  • impl Trait for Type: Syntax for implementing traits on types
  • Method signature: &self for methods that borrow the instance
  • Return types: Traits can specify return types like fmt::Result

7. Associated Functions vs Methods

#![allow(unused)]
fn main() {
impl UserId {
    pub fn new(id: u32) -> Self {        // Associated function
        UserId(id)
    }

    pub fn as_u32(&self) -> u32 {        // Method
        self.0
    }
}
}
  • Associated functions: Don't take self, called with Type::function()
  • Methods: Take some form of self, called with instance.method()
  • Self type: Refers to the implementing type (here, UserId)
  • Constructor pattern: new() is conventionally an associated function

8. Pattern Matching and Option Type

#![allow(unused)]
fn main() {
pub fn domain(&self) -> Option<&str> {
    self.0.split('@').nth(1)  // Returns Option<&str>
}
}
  • Option<T>: Represents values that might not exist
  • Some(value): Contains a value
  • None: Represents absence of value
  • No null pointers: Rust eliminates null pointer exceptions
  • Explicit handling: Must handle both Some and None cases

9. Visibility and Encapsulation

#![allow(unused)]
fn main() {
pub struct UserId(u32);  // Public struct, but field is private by default
}
  • pub: Makes items publicly visible
  • Default privacy: Struct fields are private unless marked pub
  • Controlled access: Use methods to provide controlled access to data
  • Encapsulation: Hide implementation details while exposing safe interfaces

10. Type Safety Without Runtime Cost

#![allow(unused)]
fn main() {
// This won't compile - different types despite same underlying data:
// assert_eq!(user_id, another_different_type_id);  // Compile error!
}
  • Strong typing: Types that wrap the same data are still distinct
  • Compile-time guarantees: Type errors caught before runtime
  • Zero-cost abstractions: Safety checks happen at compile time
  • Performance: No runtime type checking needed

These fundamental concepts form the foundation for all Rust programming and will appear throughout the remaining pattern chapters. Understanding them here will make the subsequent patterns much clearer.