name: rust-type-system
description: | Use when this capability is needed.
metadata:
author: adxptived
Quick Navigation
Rust Type-Driven Design
Make invalid states unrepresentable. If the compiler can catch it, don't leave it to runtime.
Core Question
Before reaching for if checks or runtime panics:
- Can the compiler reject invalid inputs?
- Can invalid states be unrepresentable in the type system?
- Can I validate at construction and trust invariants everywhere else?
Newtype Pattern
Wrap primitives to create distinct, incompatible types:
fn transfer(from: u64, to: u64, amount: u64) { ... }
transfer(order_id, user_id, cents);
struct UserId(u64);
struct OrderId(u64);
struct Cents(u64);
fn transfer(from: UserId, to: UserId, amount: Cents) { ... }
transfer(order_id, user_id, amount);
Newtype with Validation (Parse, Don't Validate)
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Email(String);
impl Email {
pub fn new(s: &str) -> Result<Self, EmailError> {
if s.contains('@') && s.split('@').nth(1).map_or(false, |d| d.contains('.')) {
Ok(Email(s.to_lowercase()))
} else {
Err(EmailError::Invalid(s.to_string()))
}
}
pub fn as_str(&self) -> &str { &self.0 }
}
impl std::ops::Deref for Email {
type Target = str;
fn deref(&self) -> &str { &self.0 }
}
fn send_email(to: &Email, body: &str) { }
Non-Empty Collections
pub struct NonEmpty<T> {
head: T,
tail: Vec<T>,
}
impl<T> NonEmpty<T> {
pub fn new(head: T) -> Self { Self { head, tail: vec![] } }
pub fn push(&mut self, item: T) { self.tail.push(item); }
pub fn first(&self) -> &T { &self.head }
pub fn len(&self) -> usize { 1 + self.tail.len() }
}
Typestate Pattern
Encode state transitions in the type system. Invalid transitions don't compile.
struct Draft;
struct PendingReview;
struct Published;
struct Post<State> {
content: String,
_state: std::marker::PhantomData<State>,
}
impl Post<Draft> {
pub fn new(content: String) -> Self {
Self { content, _state: std::marker::PhantomData }
}
pub fn edit(&mut self, content: String) {
self.content = content;
}
pub fn submit(self) -> Post<PendingReview> {
Post { content: self.content, _state: std::marker::PhantomData }
}
}
impl Post<PendingReview> {
pub fn approve(self) -> Post<Published> {
Post { content: self.content, _state: std::marker::PhantomData }
}
pub fn reject(self) -> Post<Draft> {
Post { content: .content, _state: std::marker::PhantomData }
}
}
<Published> {
(&) & { &.content }
}
= Post::(.());
post.(.());
= post.();
= post.();
(, post.());
Typestate Builder (Required Fields at Compile Time)
struct NoUrl;
struct HasUrl(String);
struct NoMethod;
struct HasMethod(Method);
struct RequestBuilder<U, M> {
url: U,
method: M,
headers: Vec<(String, String)>,
timeout_secs: u64,
}
impl RequestBuilder<NoUrl, NoMethod> {
pub fn new() -> Self {
Self { url: NoUrl, method: NoMethod, headers: vec![], timeout_secs: 30 }
}
}
impl<M> RequestBuilder<NoUrl, M> {
pub fn url(self, url: impl Into<String>) -> RequestBuilder<HasUrl, M> {
RequestBuilder { url: HasUrl(url.into()), method: self.method,
headers: self.headers, timeout_secs: self.timeout_secs }
}
}
impl<U> RequestBuilder<U, NoMethod> {
pub fn method(, method: Method) RequestBuilder<U, HasMethod> {
RequestBuilder { url: .url, method: (method),
headers: .headers, timeout_secs: .timeout_secs }
}
}
<U, M> RequestBuilder<U, M> {
( , key: <>, val: <>) {
.headers.((key.(), val.()));
}
( , secs: ) {
.timeout_secs = secs;
}
}
<HasUrl, HasMethod> {
() Request {
Request { url: .url., method: .method.,
headers: .headers, timeout_secs: .timeout_secs }
}
}
= RequestBuilder::()
.()
.(Method::GET)
.(, )
.();
State Machine with Enums
For runtime state (when typestate is too rigid):
#[derive(Debug)]
pub enum Connection {
Disconnected,
Connecting { attempt: u32, started_at: std::time::Instant },
Connected { session_id: String, established_at: std::time::Instant },
Reconnecting { attempts: u32, last_error: String },
}
impl Connection {
pub fn connect(&self) -> Result<Connection, ConnectError> {
match self {
Connection::Disconnected => Ok(Connection::Connecting {
attempt: 1,
started_at: std::time::Instant::now(),
}),
Connection::Connected { .. } => Err(ConnectError::AlreadyConnected),
other => Err(ConnectError::InvalidState(format!("{other:?}"))),
}
}
pub fn is_usable(&self) -> bool {
matches!(self, Connection::Connected { .. })
}
}
PhantomData
PhantomData<T> adds type information without runtime cost:
use std::marker::PhantomData;
struct Token<User> {
value: String,
_user: PhantomData<User>,
}
struct AdminUser;
struct RegularUser;
fn admin_action(token: &Token<AdminUser>) { }
Sealed Traits
Prevent external implementations — stable extension point without full openness:
mod private {
pub trait Sealed {}
}
pub trait DatabaseDriver: private::Sealed {
fn execute(&self, query: &str) -> Result<Rows, DbError>;
}
pub struct PostgresDriver { }
impl private::Sealed for PostgresDriver {}
impl DatabaseDriver for PostgresDriver { }
Non-Exhaustive Enums
Allow adding variants without breaking downstream:
#[non_exhaustive]
pub enum Event {
Click { x: i32, y: i32 },
KeyPress(char),
Resize { width: u32, height: u32 },
}
match event {
Event::Click { x, y } => handle_click(x, y),
Event::KeyPress(c) => handle_key(c),
Event::Resize { width, height } => handle_resize(width, height),
_ => {}
}
repr(transparent)
For newtypes that need identical memory layout to the wrapped type (FFI, safety):
#[repr(transparent)]
pub struct NonZeroU32(u32);
The Never Type (!)
Use the never type ! to indicate computations that diverge (i.e. never return). It can be coerced to any other type.
fn loop_forever() -> ! {
loop {
}
}
PhantomData Marker Types
Use std::marker::PhantomData to tell the compiler that a struct behaves as if it owns a value of type T even if it only uses it at compile time (e.g. for lifetime bounds or variance assertions).
use std::marker::PhantomData;
pub struct Serializer<T> {
format: String,
_marker: PhantomData<T>,
}
impl<T> Serializer<T> {
pub fn new(format: String) -> Self {
Self {
format,
_marker: PhantomData,
}
}
}
Pattern Selection Guide
| Need | Pattern |
|---|
| Prevent ID swaps | Newtype |
| Validate at boundary | Newtype + new() returning Result |
| Required builder fields | Typestate Builder |
| State machine (compile-time) | Typestate with PhantomData |
| State machine (runtime) | Enum with transitions |
| Prevent external trait impl | Sealed Trait |
| Future-proof enum | #[non_exhaustive] |
| Generic type info, no data | PhantomData |
Anti-Patterns
fn connect(use_tls: bool, verify_cert: bool) {}
connect(false, true);
enum TlsMode { Disabled, InsecureForLocalDev, Verified }
fn connect(tls: TlsMode) {}
fn load(user_id: u64, order_id: u64) {}
struct UserId(u64);
struct OrderId(u64);
fn load(user_id: UserId, order_id: OrderId) {}
Design Checklist
- Use newtypes for domain IDs, units, validated strings, and external identifiers.
- Prefer enums over stringly-typed or boolean state.
- Use typestate only when it prevents real misuse at compile time.
- Keep marker types zero-sized and private unless downstream users need them.
- Document invariants that unsafe code or FFI depends on.
- Add compile-fail examples or tests for APIs whose safety comes from type restrictions.
References
Source: adxptived/Rust-Skills — distributed by TomeVault.