#Type System Patterns
Rust's type system is one of the most expressive among systems programming languages. This chapter covers practical patterns that leverage the type system for safety and clarity.
#Newtype Pattern
Wrap primitive types to add semantic meaning and prevent mix-ups:
struct Meters(f64);
struct Seconds(f64);
fn speed(distance: Meters, time: Seconds) -> f64 {
distance.0 / time.0
}
let distance = Meters(100.0);
let time = Seconds(9.58);
let velocity = speed(distance, time);
This prevents accidentally passing meters where seconds are expected — the compiler catches it.
#Builder Pattern
Use the builder pattern for types with many optional fields:
struct Server {
host: String,
port: u16,
max_connections: usize,
timeout_seconds: u64,
}
struct ServerBuilder {
host: String,
port: u16,
max_connections: usize,
timeout_seconds: u64,
}
impl ServerBuilder {
fn new(host: impl Into<String>, port: u16) -> Self {
Self {
host: host.into(),
port,
max_connections: 100,
timeout_seconds: 30,
}
}
fn max_connections(mut self, count: usize) -> Self {
self.max_connections = count;
self
}
fn timeout(mut self, seconds: u64) -> Self {
self.timeout_seconds = seconds;
self
}
fn build(self) -> Server {
Server {
host: self.host,
port: self.port,
max_connections: self.max_connections,
timeout_seconds: self.timeout_seconds,
}
}
}
let server = ServerBuilder::new("localhost", 8080)
.max_connections(500)
.timeout(60)
.build();
#Typestate Pattern
Encode state machines in the type system so invalid transitions are compile-time errors:
struct Draft;
struct Review;
struct Published;
struct Article<State> {
title: String,
body: String,
_state: std::marker::PhantomData<State>,
}
impl Article<Draft> {
fn new(title: String, body: String) -> Self {
Self {
title,
body,
_state: std::marker::PhantomData,
}
}
fn submit_for_review(self) -> Article<Review> {
Article {
title: self.title,
body: self.body,
_state: std::marker::PhantomData,
}
}
}
impl Article<Review> {
fn approve(self) -> Article<Published> {
Article {
title: self.title,
body: self.body,
_state: std::marker::PhantomData,
}
}
fn reject(self) -> Article<Draft> {
Article {
title: self.title,
body: self.body,
_state: std::marker::PhantomData,
}
}
}
impl Article<Published> {
fn url(&self) -> String {
format!("/articles/{}", self.title.to_lowercase().replace(' ', "-"))
}
}
#Trait Objects vs Generics
Use generics for static dispatch (faster, monomorphized) and trait objects for dynamic dispatch (flexible, smaller binaries):
trait Renderer {
fn render(&self, content: &str) -> String;
}
// Static dispatch — resolved at compile time
fn render_static<R: Renderer>(renderer: &R, content: &str) -> String {
renderer.render(content)
}
// Dynamic dispatch — resolved at runtime
fn render_dynamic(renderer: &dyn Renderer, content: &str) -> String {
renderer.render(content)
}
// Owned trait objects for collections of mixed types
fn render_all(renderers: &[Box<dyn Renderer>], content: &str) -> Vec<String> {
renderers.iter().map(|renderer| renderer.render(content)).collect()
}
#Enums as Algebraic Data Types
Rust enums model sum types — values that can be one of several variants:
enum Command {
Quit,
Echo(String),
Move { x: f64, y: f64 },
Color(u8, u8, u8),
}
fn execute(command: Command) {
match command {
Command::Quit => std::process::exit(0),
Command::Echo(message) => println!("{message}"),
Command::Move { x, y } => println!("Moving to ({x}, {y})"),
Command::Color(red, green, blue) => {
println!("Setting color to rgb({red}, {green}, {blue})")
}
}
}
#Summary
These patterns leverage Rust's type system to catch bugs at compile time rather than runtime. The newtype and typestate patterns are particularly powerful — they encode domain rules directly in the type system, making invalid states unrepresentable.