Skip to content

Optional Generic Members ​

Generic type parameters, impl Trait or const generics used exclusively in optional members break type inference. This problem becomes visible when you skip setting an optional member.

🔴 Bad ​

rust
#[bon::builder]
fn bad<T: Into<String>>(x1: Option<T>) {
    let x1 = x1.map(Into::into);
    // ...
}

// This compiles
bad().x1("&str").call();

// This doesn't
bad().call(); // error[E0283]: type annotations needed

The compilation error here is:

rust
bad().call();
^^^ cannot infer type of the type parameter `T` declared on the function `bad`

A similar error would be generated if we used Option<impl Into<String>>, although the error would reference a generic parameter auto-generated for the function by the builder macro. For simplicity, we'll use a named generic parameter throughout the examples.

The caller of your builder would need to work around this problem by specifying the type T explicitly via turbofish:

rust
// Both String or `&str` would work as a type hint
bad::<String>().call();

This is inconvenient, don't do this.

🟢 Good ​

Instead, make the member's type non-generic and move generics to the setter methods' signature. Let's see what it means with an example.

For the case above, the good solution is #[builder(into)].

rust
#[bon::builder]
fn good(#[builder(into)] x1: Option<String>) {
    // ...
}

good().x1("&str").call();
good().call();

How #[builder(into)] is different from Option<T> (T: Into)? Let's compare the generated code between them (simplified). Switch between the tabs below:

rust
fn good() -> GoodBuilder { /**/ }

impl<S: State> GoodBuilder<S> {
    fn x1(self, value: impl Into<String>) -> GoodBuilder<SetX1<S>> {
        GoodBuilder { /* other fields */, __x1: value.into() }
    }
}
rust
fn bad<T>() -> BadBuilder<T> { /**/ }

impl<T: Into<String>, S: State> BadBuilder<T, S> {
    fn x1(self, value: T) -> BadBuilder<T, SetX1<S>> {
        BadBuilder { /* other fields */, __x1: value }
    }
}

Notice how in the builder(into) example the starting function good() doesn't declare any generic parameters, while in the Option<T> example bad() does have a generic parameter T.

Also, in the case of builder(into) the call to .into() happens inside of the setter method itself (early). In the case of Option<T>, the call to .into() is deferred to the finishing function.

This is also visible when you compare the original functions again. Notice how in the Good example we already have a concrete Option<String> type while in the Bad example, we have to manually call x1.map(Into::into):

rust
#[bon::builder]
fn good(#[builder(into)] x1: Option<String>) {
    // ...
}
rust
#[bon::builder]
fn bad<T: Into<String>>(x1: Option<T>) {
    let x1 = x1.map(Into::into);
    // ...
}

So, the builder has to carry the generic parameter T in its type for you to be able to use that type in your function body to merely invoke Into::into on a parameter. Instead, strive to do such conversions in setters.

If you are doing a conversion with something other than Into, then use #[builder(with)] to apply the same technique for getting rid of generic parameters early.