Menu
Coddy logo textTech

Newtype Pattern

Part of the Object Oriented Programming section of Coddy's Rust journey — lesson 54 of 61.

Sometimes you have two values of the same type that represent completely different things. A user's email and their password might both be String values, but accidentally swapping them in a function call would be a serious bug. The Newtype pattern solves this by creating distinct types that wrap primitives.

A newtype is simply a tuple struct with a single field:

struct Password(String);
struct Email(String);

Even though both wrap a String, the compiler treats them as completely different types. You can't accidentally pass a Password where an Email is expected—the compiler will catch the mistake.

fn send_reset_email(email: Email, password: Password) {
    // ...
}

let email = Email(String::from("user@example.com"));
let pass = Password(String::from("secret123"));

send_reset_email(email, pass);  // Correct order enforced by types
// send_reset_email(pass, email);  // Won't compile!

To access the inner value, use .0 since it's a tuple struct:

let pass = Password(String::from("secret123"));
println!("Length: {}", pass.0.len());

You can also add methods to your newtype, giving it behavior specific to what it represents—like validation or formatting—that wouldn't make sense on a plain String.

challenge icon

Challenge

Easy

Let's build a type-safe user registration system using the Newtype pattern! You'll create distinct types for Username and UserId to prevent accidentally mixing them up—even though both wrap simple values.

You'll organize your code across two files:

  • user_types.rs: Define two public newtype structs—Username wrapping a String and UserId wrapping a u32. Add a method called value to each that returns a reference to the inner data (for Username, return &String; for UserId, return &u32). This gives controlled access to the wrapped values.
  • main.rs: Bring in your user_types module and create a function called display_user that takes a UserId as the first parameter and a Username as the second parameter. The function should print the user information. Then create instances of both types using the provided inputs and call your function.

The key insight here is that even though UserId and Username are simple wrappers, the compiler treats them as completely different types. You can't accidentally pass a username where an ID is expected!

Your display_user function should print in this format:

User #{id}: {username}

For example, with inputs 42 and alice_dev:

User #42: alice_dev

And with inputs 1001 and bob_smith:

User #1001: bob_smith

You will receive two inputs: the user ID (parse as u32) and the username string.

Cheat sheet

The Newtype pattern creates distinct types by wrapping primitives in tuple structs, preventing accidental misuse of values that have the same underlying type but different meanings.

Define a newtype as a tuple struct with a single field:

struct Password(String);
struct Email(String);

Even though both wrap a String, the compiler treats them as completely different types:

fn send_reset_email(email: Email, password: Password) {
    // ...
}

let email = Email(String::from("user@example.com"));
let pass = Password(String::from("secret123"));

send_reset_email(email, pass);  // Correct
// send_reset_email(pass, email);  // Won't compile!

Access the inner value using .0:

let pass = Password(String::from("secret123"));
println!("Length: {}", pass.0.len());

You can add methods to newtypes for type-specific behavior like validation or controlled access to the wrapped value.

Try it yourself

mod user_types;

use user_types::{Username, UserId};

// TODO: Create a function called `display_user` that takes:
// - First parameter: UserId
// - Second parameter: Username
// The function should print: "User #{id}: {username}"
// Use the .value() method to access the inner values


fn main() {
    // Read input
    let mut id_input = String::new();
    std::io::stdin().read_line(&mut id_input).expect("Failed to read line");
    let id: u32 = id_input.trim().parse().expect("Invalid number");
    
    let mut username_input = String::new();
    std::io::stdin().read_line(&mut username_input).expect("Failed to read line");
    let username = username_input.trim().to_string();
    
    // TODO: Create instances of UserId and Username using the inputs
    
    // TODO: Call display_user with the correct arguments
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming