Type-State Builders
Encode validity in the type system.
Type-State Builders is a free Learn Rust Coding lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Learn Rust Coding learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Problem with Plain Builders
A normal builder lets you call build() at any time, even before required fields are set. Missing data then surfaces as a runtime panic or an Err.
Type-state builders push that check to compile time: forgetting a required step simply fails to compile.
Encoding State in Types
The trick is to make the builder generic over marker types that represent which steps are done. As you set each field, the builder's type changes.
Only when every required marker reaches its "set" state does a build() method become available.
struct Missing;
struct Set;A Builder Generic Over Markers
Give the builder type parameters for each required field's state. PhantomData carries the marker without storing any real data.
use std::marker::PhantomData;
struct ReqBuilder<H, U> {
url: Option<String>,
method: Option<String>,
_state: PhantomData<(H, U)>,
}The Starting State
The constructor returns a builder where every required marker is Missing. At this point no build() exists, so the type system knows the object is incomplete.
impl ReqBuilder<Missing, Missing> {
fn new() -> Self {
ReqBuilder { url: None, method: None, _state: PhantomData }
}
}Transitioning a Marker
A setter consumes the old builder and returns a new one with that field's marker flipped to Set. The other marker is preserved by keeping its type parameter generic.
impl<U> ReqBuilder<Missing, U> {
fn url(self, url: &str) -> ReqBuilder<Set, U> {
ReqBuilder { url: Some(url.to_string()),
method: self.method, _state: PhantomData }
}
}The Second Transition
Setting the method works the same way, flipping the second marker from Missing to Set while leaving the first untouched.
impl<H> ReqBuilder<H, Missing> {
fn method(self, m: &str) -> ReqBuilder<H, Set> {
ReqBuilder { url: self.url,
method: Some(m.to_string()), _state: PhantomData }
}
}build() Only When Fully Set
Crucially, build() is implemented only for ReqBuilder<Set, Set>. Any other state simply has no such method, so the call fails to compile.
Inside, the unwrap calls can never panic because the type proves both fields are present.
struct Request { url: String, method: String }
impl ReqBuilder<Set, Set> {
fn build(self) -> Request {
Request { url: self.url.unwrap(), method: self.method.unwrap() }
}
}Putting It Together
A correct chain compiles cleanly because each call moves the builder toward ReqBuilder<Set, Set>, where build() exists.
fn demo() -> Request {
ReqBuilder::new()
.url("https://example.com")
.method("GET")
.build()
}The Compile Error You Want
Skip a required step and the compiler refuses. Calling build() on ReqBuilder<Set, Missing> reports "no method named build", catching the omission before the program ever runs.
// ReqBuilder::new().url("x").build();
// error: no method named `build` found for
// ReqBuilder<Set, Missing>A Self-Contained Runnable Version
This minimal program uses a single required field to show the full transition end to end. It compiles and prints the built value.
use std::marker::PhantomData;
struct Missing; struct Set;
struct B<N> { name: Option<String>, _s: PhantomData<N> }
impl B<Missing> {
fn new() -> Self { B { name: None, _s: PhantomData } }
fn name(self, n: &str) -> B<Set> {
B { name: Some(n.to_string()), _s: PhantomData }
}
}
impl B<Set> {
fn build(self) -> String { self.name.unwrap() }
}
fn main() {
let v = B::new().name("prod").build();
println!("built: {}", v);
}Costs and Trade-Offs
Type-state builders give compile-time guarantees with zero runtime overhead, since markers are erased. The price is more type machinery and combinatorial impl blocks as required fields grow.
Reserve the pattern for APIs where an incomplete build must be impossible by construction.
Quick Check
Identify what actually enforces completeness in a type-state builder.
Recap
Type-state builders encode each required step as a marker type parameter, carried by PhantomData. Setters consume the builder and return a new type with one marker flipped to Set.
Because build() is implemented only for the all-Set state, forgetting a step is a compile error, not a runtime panic, all at zero runtime cost.
Frequently asked questions
Is the “Type-State Builders” lesson free?
Yes — the full text of “Type-State Builders” is free to read here on the web, and the Learn Rust Coding course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Learn Rust Coding course, upgrade to CoddyKit PRO.
What will I learn in “Type-State Builders”?
Encode validity in the type system. You practise Learn Rust Coding with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Learn Rust Coding?
No prior experience is required. Learn Rust Coding on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Type-State Builders” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Learn Rust Coding lesson?
Yes. Every Learn Rust Coding lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- The Builder Pattern
- The Newtype Pattern
- Type-State Builders
- Deref and Wrapper Ergonomics