Open-source project
idanarye/rust-typed-builder avatar
idanarye/rust-typed-builder

typed-builder: a Rust builder that checks itself at compile time

Compile-time type-checked builder derive

1,203 stars60 forksRustApache-2.0

At a glance

What is it?
typed-builder is a derive macro that generates a builder for a struct and refuses to compile if a mandatory field is missing or a field is set twice, so the failure lands in the type checker rather than in a runtime Result. It suits a Rust codebase with structs that have many optional fields, and it deliberately gives up on being a builder you can pass around.
Who is it for?
Adopt typed-builder when you have structs with many fields where the mandatory and optional ones are worth distinguishing at the type level, and you want a missing field to be a compile error rather than a runtime unwrap. Skip it if you need a builder you can store, clone or thread through a function, because the generated type is not meant for passing around and the builder is not cloneable.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 85 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The builder that fails in the type checker

typed-builder creates a compile-time verified builder, and the claim is about where the error appears. With a runtime builder you call `build()` and get back a `Result` that you have to unwrap, and the mistake surfaces when that unwrap panics, often in a code path no test covered. With a derive, the mistake is a compile error, before the binary exists. The usage is a derive on the struct and a `builder()` method. The struct in the README has a mandatory field `x`, an optional field `y` of type `Option<i32>` marked with `#[builder(default, setter(strip_option))]`, and a field `z` with an explicit default of 20:

rust
use typed_builder::TypedBuilder;

#[derive(TypedBuilder)]
struct Foo {
    // Mandatory Field:
    x: i32,

    // #[builder(default)] without parameter - use the type's default
    // #[builder(setter(strip_option))] - wrap the setter argument with `Some(...)`
    #[builder(default, setter(strip_option))]
    y: Option<i32>,

    // Or you can set the default
    #[builder(default=20)]
    z: i32,
}

The two things the derive gives you are compile time verification that all fields are set before calling `.build()` and compile time verification that no field is set more than once. The primary language is Rust, the licence is dual Apache-2.0 or MIT, and the crate is `typed-builder` on crates.io with the macro in a sibling `typed-builder-macro` crate.

Order does not matter, omission does

The ergonomics are demonstrated by three short examples. Build in any order, so `Foo::builder().x(1).y(2).z(3).build()` and `Foo::builder().z(1).x(2).y(3).build()` are both fine. Omit the optional field, the one marked with `#[default]`, and `Foo::builder().x(1).build()` compiles. And the two mistakes that do not compile are `Foo::builder().build()` for a missing `x`, and `Foo::builder().x(1).y(2).y(3)` for a `y` specified twice. The reason order is free is worth understanding, because it is the trick behind the whole crate. The builder's state is encoded in the generics arguments rather than in a runtime flag, so the type of the builder object changes as fields are set, and the type of the value you get back changes to reflect which fields are already present. Rust only has to generate the code path you actually use.

The three attribute forms and what they do

The attributes are the whole configuration surface, and there are three distinct things they express. `#[builder(default)]` without a parameter makes the field optional and uses the type's own default, which is the `Option<i32>` case. `#[builder(default=20)]` sets an explicit default value, which is the `z` case where the field is a plain `i32` with a fallback of 20. And `#[builder(setter(strip_option))]` changes the setter signature so that it takes the inner type rather than the `Option`, with the setter wrapping the argument in `Some(...)` for you, so you call `.y(2)` rather than `.y(Some(2))`. The `setter(into)` variant, listed in the features, makes a setter accept any value that converts into the field type through `Into`. There is also a generated simple documentation for the `.builder()` method, and the method name and the visibility of the `.build()` method are both customizable.

Why the state lives in the type parameters

The alternatives section explains the design choice better than the feature list does, and it is the part to read before choosing a crate. derive-builder does all the checks at runtime, returning a `Result` you need to unwrap. safe-builder-derive does compile-time checks by generating a type for each possible state of the builder, and Rust can remove the dead code, but your build time will still be exponential. typed-builder encodes the builder's state in the generics arguments, so Rust will only generate the path you actually use. That is the entire difference in one sentence. The cost of the approach is that the generated builder type has an ugly internal name and many generic parameters, and the README is explicit that it is not meant for passing around and doing fancy builder tricks, only for nicer object creation syntax with named arguments and optional arguments.

The documented limitations, and the state of the error message

There are two limitations and both are worth knowing before you adopt. The first is about error reporting. The build errors when you neglect to set a field or set a field describe the actual problem as a deprecation warning, not as the main error. In practice that means the compiler output leads with something else and the real reason, that a field is missing or set twice, appears as a deprecation note further down, so you read the whole message rather than the first line. The second is the consequence of the type-parameter design, already noted, and it is reinforced in the features list: all builder methods are call-by-move and the builder is not cloneable, which the README says saves the trouble of determining if the fields are cloneable. If you want a builder you can pass around, the README points at derive-builder, and adds that its API does not conflict with typed-builder's, so you can implement both on the same type.

The conflict with other derive macros

TypedBuilder accepts arbitrary Rust code for `#[builder(default = ...)]`, and that is the source of a specific incompatibility. Other custom derive proc-macro crates may try to parse those attributes using the older restrictions that allow only literals, and when they do, the parse fails on your `default = 20` style expression. The documented workaround is to use `#[builder(default_code = "...")]` instead, which hands the code over as a string that the other macro is less likely to try to interpret. This is a niche problem, and it only bites if you combine typed-builder with another derive macro that scans builder-style attributes on the same struct, but the failure mode is confusing, because the error comes from the other crate's parser and points at your typed-builder attribute. The repository also carries example files that show the shape of the API, with `examples/example.rs`, `examples/complicate_build.rs`, `examples/example_prefix_suffix.rs` for the customised method names and `examples/example_for_linting.rs`.

Editorial conclusion

Adopt typed-builder when you have structs with many fields where the mandatory and optional ones are worth distinguishing at the type level, and you want a missing field to be a compile error rather than a runtime unwrap. Skip it if you need a builder you can store, clone or thread through a function, because the generated type is not meant for passing around and the builder is not cloneable. Before you switch, note the two documented rough edges, that a missing or duplicated field is reported as a deprecation warning rather than the main error, and that other derive macro crates may misparse `#[builder(default = ...)]`, where the `default_code` attribute is the workaround.

Frequently asked questions

What does typed-builder guarantee at compile time?

That all fields are set before calling .build(), and that no field is set more than once. A builder call that omits a mandatory field or sets one twice does not compile.

How do I give a field a default in typed-builder?

Use `#[builder(default)]` to use the type's own default, `#[builder(default=20)]` to set an explicit value, and `#[builder(setter(strip_option))]` to make an Option field's setter take the inner type. `#[builder(setter(into))]` makes a setter accept any value that converts with Into.

Why is typed-builder faster to compile than safe-builder-derive?

safe-builder-derive generates a type for each possible state of the builder, which makes build time exponential even though Rust removes the dead code. typed-builder encodes the state in the generics arguments, so Rust only generates the path you actually use.

Can I use a typed-builder builder after passing it to a function?

Not usefully. The generated builder type is not meant for passing around, all builder methods are call-by-move and the builder is not cloneable. For a builder you can pass around the README points at derive-builder, whose API does not conflict with typed-builder's.

What licence is typed-builder released under?

Dual licensed, Apache License 2.0 or the MIT license, at your option, with the manifest recording MIT OR Apache-2.0 and both LICENSE-APACHE and LICENSE-MIT in the repository.

Official sources

  1. idanarye/rust-typed-builder on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/idanarye-rust-typed-builder.svg)](https://hysenlabs.com/projects/idanarye-rust-typed-builder)

Was this useful?