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
EasyLet'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—Usernamewrapping aStringandUserIdwrapping au32. Add a method calledvalueto 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 calleddisplay_userthat takes aUserIdas the first parameter and aUsernameas 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_devAnd with inputs 1001 and bob_smith:
User #1001: bob_smithYou 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
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Methods and Behavior
Intro Implementation BlocksThe Self ParameterMutable MethodsAssociated FunctionsMultiple Implementation BlocksMethod ChainingRecap - Rectangle Actions4Project: Virtual Pet
Defining the PetFeeding the Pet7Standard Traits
The Debug TraitThe Display TraitClone and CopyEquality TraitsRecap - Printable Point10Project: Document System
The Draw TraitText Component2Encapsulation and Modules
Modules BasicsThe Public KeywordPrivate FieldsGettersSettersRecap - Secure Locker5Generics
Generic StructsGeneric MethodsMultiple Generic TypesGeneric FunctionsRecap - Coordinate Point8Traits as Bounds
Trait Bounds SyntaxMultiple BoundsThe Where ClauseReturning Types with TraitsRecap - Generic Printer11Design Patterns in Rust
Newtype PatternCompositionThe Drop TraitFrom and IntoRecap - Smart Pointer Mock