Type-Level API Design
Type-level API design uses generic parameters, associated types, phantom markers, and trait bounds to encode domain rules into function and struct signatures so invalid configurations, mismatched units, and unverified permissions fail to compile rather than failing in production.
Core Type-Level Design Techniques
| Technique | Mechanism | Runtime Cost | Example |
|---|---|---|---|
| Capability / Permission Tokens | Require a zero-sized AdminCapability value (constructible only by an auth check) as a function argument |
0 bytes (ZST) |
fn delete_user(cap: &AdminCap, id: UserId) |
| Type-State Builders | Track which required fields have been set via type parameters Builder<HasHost, HasPort> |
0 bytes (PhantomData) |
.build() only exists on Builder<Set, Set> |
Heterogeneous Type Lists ((A, (B, ()))) |
Recursive nested tuples implementing a common trait | 0 dynamic dispatch overhead (fully inlined) |
Middleware chains and ECS query tuples |
| Non-Polymorphic Inner De-generification | Thin generic wrapper calls a non-generic inner function taking &[u8] or &Path |
Eliminates monomorphization code bloat | fn open(p: impl AsRef<Path>) { open_inner(p.as_ref()) } |
Compile-Time Verified Builder (Required Fields Checked Without Option::unwrap)
A traditional builder stores Option<T> for required fields and returns a Result (or panics) inside .build() if a caller forgets to set a required field. A type-level builder tracks each required field with a Unset / Set<T> type parameter so .build() is only callable when every required field is Set<T>, returning Config directly instead of Result<Config, BuilderError>:
pub struct Unset;
pub struct Set<T>(T);
pub struct ServerBuilder<Host, Port> {
host: Host,
port: Port,
max_threads: usize,
}
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub max_threads: usize,
}
impl ServerBuilder<Unset, Unset> {
pub fn new() -> Self {
Self {
host: Unset,
port: Unset,
max_threads: 4,
}
}
}
impl<Host, Port> ServerBuilder<Host, Port> {
pub fn host(self, host: impl Into<String>) -> ServerBuilder<Set<String>, Port> {
ServerBuilder {
host: Set(host.into()),
port: self.port,
max_threads: self.max_threads,
}
}
pub fn port(self, port: u16) -> ServerBuilder<Host, Set<u16>> {
ServerBuilder {
host: self.host,
port: Set(port),
max_threads: self.max_threads,
}
}
// Optional setting works in ANY `(Host, Port)` state:
pub fn max_threads(mut self, threads: usize) -> Self {
self.max_threads = threads;
self
}
}
// `.build()` is ONLY implemented when both `Host` and `Port` are `Set<...>`:
impl ServerBuilder<Set<String>, Set<u16>> {
pub fn build(self) -> ServerConfig {
ServerConfig {
host: self.host.0,
port: self.port.0,
max_threads: self.max_threads,
}
}
}
fn main() {
// `.host()` and `.port()` can be called in ANY order, and `.build()`
// returns `ServerConfig` directly without `Result` or `unwrap()`!
let cfg = ServerBuilder::new()
.port(8080)
.max_threads(8)
.host("0.0.0.0")
.build();
println!(
"configured {}:{} (threads = {})",
cfg.host, cfg.port, cfg.max_threads
);
}
configured 0.0.0.0:8080 (threads = 8)
Minimizing Monomorphization Bloat with the Inner-Function Trick
When a public function uses generics purely for caller convenience—such as pub fn read_config(path: impl AsRef<Path>)—placing a 200-line body inside the generic function compiles a separate copy of those 200 lines for &str, String, &Path, and PathBuf. Delegating immediately to a private non-generic fn inner(path: &Path) compiles the conversion inline and compiles the 200-line body once:
use std::path::Path;
pub fn load_manifest(path: impl AsRef<Path>) -> String {
// Non-generic inner function is compiled only once across the entire binary:
fn inner(path: &Path) -> String {
format!("loaded manifest from {:?}", path.display())
}
inner(path.as_ref())
}
fn main() {
println!("{}", load_manifest("Cargo.toml"));
println!("{}", load_manifest(String::from("src/lib.rs")));
}
loaded manifest from "Cargo.toml"
loaded manifest from "src/lib.rs"