What is “newtype” Pattern in Rust…?
Rust’s design patterns are a bit different from the classic OOP, and those patterns are based on concepts like Ownership, Borrowing, and Zero-Cost Abstractions… 🦀
At the bottom level, A design pattern is a reusable, general solution to a commonly occurring problem in software design. Something like best practices…
newtype
Wrap a primitive like i32, f64, String… in a struct. It’s like labeling a data type with a domain name.
For example,
If you have an email address, it should be a String obviously. What we do here is wrap the type with a label like Email(String) and define it as a struct.
Now the email becomes a data type itself!
If you’re familiar with Firebird, you’ll probably notice defining domains.
struct Email(String);So what is the reason for something like this…?
Push correctness checks into the type system so the compiler catches mistakes when writing the code, so you can write less error-prone, accurate, much better code :)
Get Isuru Sathsara’s stories in your inbox
Join Medium for free to get updates from this writer.
Note
The code snippets below are simplified to focus strictly on the idea rather than boilerplate.
Prevent argument mix-ups
The core reason is Rust’s type checker only cares about type names, not the semantic meaning of underlying data. Which means, if you have a code like below,
fn transfer(amount: i32, acc_no: i32) {
// something...
}
fn main() {
let amount: i32 = 500;
let acc_no: i32 = 6514;
// swap args; works ok; incorrect result!
transfer(acc_no, amount);
}Sometimes, you may swap arguments mistakenly, and the code may compile and work because both arguments i32.
How does newtype prevent this…?
First, we define amount and acc_no as structs, and pass those structs as arguments into our transfer function, instead of their underlying data type, which is i32.
// this code is just to understand the concept;
struct Amount(i32);
struct AccNo(i32);
// pass structs as arguments
fn transfer(amount: Amount, acc_no: AccNo) {
// do something
}
fn main() {
transfer(Amount(500), AccNo(6514));
}In this way, Rust doesn’t allow you to compile the code if you try swapping.
Make domains/types more readable
struct Meters(f64);
struct Centimeters(f64);
struct Inches(f64);
struct TotalLength(f64);
struct NodeIndex(usize);
struct EdgeIndex(usize);Even without extra logic, Meters(f64) is more readable and documentable than just bare f64.
Hiding Implementation details
You can control what to expose, even though these are small data types internally.
pub struct UserName(String); // Username is public; String remains hidden;
pub struct Password(String);
struct Email(String);Here, the UserName is exposed, but String remains hidden as private.
It prevents outside code from mutating internal data directly.
This is one way encapsulation works in Rust!
Adding Validations to the Wrapper Type
For example,
Let’s take the Email struct we mentioned above and add a validation like there must be an @ sign in a valid email address…
Usually, validation should be done inside a function like new() at the time Email is created. Also note that since our Email has become a struct, we can implement functions with impl.
struct Email(String);
impl Email {
// validate when creating Email;
pub fn new(value: String) -> Result<Self, String> {
if value.contains('@') && value.contains('.') {
Ok(Email(value))
} else {
Err("Missing the `@` and/or the `.`".into())
}
}
}
fn main() {
let valid_email = Email::new("atlas@123.com".to_string());
assert!(valid_email.is_ok());
}The following code is more understandable, but it ruins the validation logic because it allows an invalid email to exist in memory before validation.
pub struct Email(String);
impl Email {
fn validate(&self) -> bool {
// self.0 --> access the inner value in struct
let res: bool = self.0.contains('@') && self.0.contains('.');
res
}
}
fn isvalid(email: Email) {
if Email::validate(&email) {
// email is valid
}
}
fn main() {
let email = Email("atlas@123.com".to_string());
isvalid(email);
}Is there any runtime cost…?
Not at all!
struct Amount(i32) has the same memory layout as bare i32 in general.
All the type checking(distinguishing Amount(i32) and AccNo(i32)) happens only at compile time. Once the compilation finishes, the distinction is dropped and the binary works with the inner type.
While it adds a tiny bit of compile-time overhead for type checking, runtime performance remains untouched.
This is a Zero Cost Abstraction. (famous concept from C++, means that using them imposes no additional runtime overhead.
Thank you for reading..!
Feel free to share any thoughts.
I might have kept unfilled gaps, but I think the article is enough to create an image in your mind about the newtype pattern :)








