TypeStateBuilder
A Rust derive macro that generates compile-time safe builders using the type-state pattern. It prevents incomplete object construction by making missing required fields a compile-time error rather than a runtime failure.
Table of Contents
- Introduction
- Design Philosophy
- Installation
- Quick Start
- Core Concepts
- Features
- Understanding Error Messages
- Compatibility
- How It Works
- License
- Contributing
Introduction
Traditional builder patterns in Rust typically validate required fields at runtime, returning Result or panicking when
fields are missing:
// Traditional builder - fails at runtime
let user = new
.name
// Forgot to set email
.build; // Returns Err or panics at runtime
TypeStateBuilder moves this validation to compile time:
// TypeStateBuilder - fails at compile time
let user = builder
.name
// Forgot to set email
.build; // Compile error: method `build` not found
The compiler error message clearly indicates what is missing:
error[E0599]: no method named `build` found for struct
`UserBuilder_HasName_MissingEmail` in the current scope
Design Philosophy
TypeStateBuilder was designed with AI-assisted development in mind. Two principles guided its design:
Compiler-Enforced Correctness
In AI-assisted development, code generation happens rapidly. LLMs can produce syntactically correct code that nonetheless contains logical errors, such as forgetting to initialize required fields. By encoding field requirements in the type system, TypeStateBuilder ensures that such errors are caught immediately by the compiler rather than manifesting as runtime failures.
The type system becomes a safety net: if the code compiles, the builder is correctly configured.
Actionable Error Messages
Many type-state builder implementations use generic type parameters to track field states:
// Other implementations might generate something like:
// ^ ^ ^ ^
// What do these mean?
When a required field is missing, the resulting error message requires decoding which type parameter corresponds to which field.
TypeStateBuilder takes a different approach. It generates a separate struct for each possible state, with the struct name explicitly describing which fields have been set and which are missing:
UserBuilder_HasName_HasEmail // Both fields set - build() available
UserBuilder_HasName_MissingEmail // Name set, email missing
UserBuilder_MissingName_HasEmail // Email set, name missing
UserBuilder_MissingName_MissingEmail // Neither field set
When an AI assistant encounters an error like UserBuilder_HasName_MissingEmail doesn't have method build, it can
immediately understand that the email field needs to be set. No documentation lookup or type parameter decoding is
required.
Trade-offs
This approach generates more structs than a type-parameter-based solution, which increases compile time slightly. However, the improved error message clarity is worth this cost, particularly in AI-assisted workflows where rapid iteration and clear feedback are essential.
Importantly, there is no runtime cost. Rust's zero-cost abstractions ensure that the generated code is as efficient as a hand-written builder.
Installation
Add TypeStateBuilder to your Cargo.toml:
[]
= "0.5.1"
no_std Support
TypeStateBuilder is compatible with no_std environments. The generated code uses only core types
(core::option::Option, core::marker::PhantomData, etc.) and does not require the standard library.
Minimum Supported Rust Version
TypeStateBuilder requires Rust 1.70.0 or later.
Quick Start
use TypeStateBuilder;
Core Concepts
Required vs Optional Fields
Fields marked with #[builder(required)] must be set before the build() method becomes available. All other fields
are optional and will use their default values if not set.
use TypeStateBuilder;
Builder Pattern Selection
TypeStateBuilder automatically selects the appropriate builder pattern based on your struct:
- Type-state builder: When the struct has required fields. The
build()method is only available after all required fields are set. - Regular builder: When all fields are optional. The
build()method is available immediately.
State Transitions
Each setter method returns a new builder type that reflects the updated state. For a struct with required fields name
and email:
builder // UserBuilder_MissingName_MissingEmail
.name // UserBuilder_HasName_MissingEmail
.email // UserBuilder_HasName_HasEmail
.build // User
Optional fields can be set at any point without affecting the type-state progression.
Features
Required Fields
Mark fields as required using the required attribute:
use TypeStateBuilder;
let config = builder
.host
.database
.build;
Default Values
Provide custom default values for optional fields:
use TypeStateBuilder;
let config = builder
.host
// port defaults to 8080
// timeout_seconds defaults to 30
// environment defaults to "production"
.build;
Skip Setter
Some fields should only use their default value without exposing a setter:
use TypeStateBuilder;
let doc = builder
.title
// id and created_at are set automatically, no setters available
.build;
Custom Setter Names
Customize individual setter method names:
use TypeStateBuilder;
let person = builder
.full_name
.years_old
.build;
Setter Prefixes
Add consistent prefixes to setter methods at the struct or field level:
use TypeStateBuilder;
let client = builder
.with_base_url
.set_api_key // Uses field-level prefix
.with_timeout
.build;
Ergonomic Conversions with impl_into
The impl_into attribute generates setters that accept impl Into<T>, allowing more ergonomic API usage:
use TypeStateBuilder;
// Apply to all fields
let config = builder
.name // &str converts to String via Into
.description // &str converts to String via Into
.id // Requires Option<u64> exactly
.build;
Custom Conversions with converter
The converter attribute provides custom transformation logic for setters:
use TypeStateBuilder;
let user = builder
.email // Normalized to "alice@example.com"
.interests // Parsed to Vec<String>
.nickname // Wrapped in Some
.build;
assert_eq!;
assert_eq!;
assert_eq!;
The converter must be a closure expression with an explicitly typed parameter:
// Correct
// Incorrect - function references are not supported
Custom Build Method Name
Customize the name of the final build method:
use TypeStateBuilder;
let conn = builder
.host
.create; // Uses custom method name
Generics and Lifetimes
TypeStateBuilder supports generic types, lifetime parameters, and complex bounds:
use TypeStateBuilder;
let text = "referenced text";
let container = builder
.data
.metadata
.reference
.build;
Const Builders
The #[builder(const)] attribute generates const fn builder methods, enabling compile-time constant construction.
This is useful for embedded systems, static configuration, and other scenarios where values must be known at compile
time.
use TypeStateBuilder;
// Compile-time constant construction
const APP_CONFIG: Config = builder
.name
.version
.port
.build;
// Also works in static context
static DEFAULT_CONFIG: Config = builder
.name
.version
.build;
// And in const fn
const
const CUSTOM: Config = make_config;
Requirements for const builders:
- Explicit defaults required: Optional fields must use
#[builder(default = expr)]becauseDefault::default()cannot be called in const context - No
impl_into: Theimpl_intoattribute is incompatible with const builders because trait bounds are not supported in const fn - Const-compatible types: Field types must support const construction (e.g.,
&'static strinstead ofString, arrays instead ofVec)
Converters with const builders:
Closure converters work with const builders. The macro automatically generates a const fn from the closure body:
use TypeStateBuilder;
const DATA: Data = builder
.name_length // Converted to 5
.doubled // Converted to 42
.build;
Builder Method Entry Point
The #[builder(builder_method)] attribute makes a required field's setter the entry point to the builder, replacing the
builder() method. This provides a more ergonomic API when one field is the natural starting point.
use TypeStateBuilder;
// Instead of User::builder().id(1).name("Alice").build()
let user = id.name.build;
assert_eq!;
assert_eq!;
Requirements:
- Only one field per struct can have
builder_method - The field must be required (not optional)
- Cannot be combined with
skip_setter
With const builders:
use TypeStateBuilder;
const APP: Config = name.version.build;
Understanding Error Messages
When a required field is missing, the compiler error includes the builder's type name, which explicitly states the current state:
error[E0599]: no method named `build` found for struct
`ConfigBuilder_MissingApi_key_MissingEndpoint` in the current scope
The naming pattern is:
{StructName}Builder_{FieldState1}_{FieldState2}_...
Where each field state is either:
Has{FieldName}- the field has been setMissing{FieldName}- the field has not been set
For example, with a struct having fields api_key and endpoint:
| State | Type Name |
|---|---|
| Neither set | ConfigBuilder_MissingApi_key_MissingEndpoint |
| Only api_key set | ConfigBuilder_HasApi_key_MissingEndpoint |
| Only endpoint set | ConfigBuilder_MissingApi_key_HasEndpoint |
| Both set | ConfigBuilder_HasApi_key_HasEndpoint |
The build() method is only available on the final state where all required fields are set.
Compatibility
no_std Support
TypeStateBuilder generates code that is compatible with no_std environments. The generated code uses:
core::option::Optioninstead ofstd::option::Optioncore::marker::PhantomDatainstead ofstd::marker::PhantomDatacore::fmt::Debuginstead ofstd::fmt::Debugcore::default::Defaultinstead ofstd::default::Default
No feature flags are required; no_std compatibility is the default.
Minimum Supported Rust Version
TypeStateBuilder requires Rust 1.70.0 or later. This requirement is driven by:
- Stable proc-macro features used in code generation
- Advanced generic parameter handling
The MSRV is tested in CI and will not be increased without a minor version bump.
How It Works
TypeStateBuilder implements the type-state pattern using Rust's type system to encode state at compile time.
Generated Types
For a struct with required fields, the macro generates:
- Multiple builder structs, one for each possible combination of set/unset required fields
- Setter methods that transition between states
- A
build()method only on the final state
State Encoding
Each required field contributes to the builder's type name. With n required fields, there are 2^n possible states.
The macro generates a struct for each state, though in practice many states are not reachable through normal usage.
Zero Runtime Cost
The state tracking is entirely compile-time:
- No runtime state variable
- No runtime validation
- No
Optionwrappers for required fields internally - The final
build()call simply moves values into the target struct
The generated code is equivalent to what you would write by hand, with the type system providing the safety guarantees.
Example Generation
For this input:
The macro generates (simplified):
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Contributing
Contributions are welcome. Please open an issue to discuss significant changes before submitting a pull request.
For bug reports and feature requests, use the issue tracker.