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.