From and Into

Let's go back to where our string journey started:

let ticket = Ticket::new(
    "A title".into(), 
    "A description".into(), 
    "To-Do".into()
);

We now know enough to start unpacking what .into() is doing here.

The problem

This is the signature of the new method:

impl Ticket {
    pub fn new(
        title: String, 
        description: String, 
        status: String
    ) -> Self {
        // [...]
    }
}

We've also seen that string literals (such as "A title") are of type &str.
We have a type mismatch here: a String is expected, but we have a &str. No magical coercion will come to save us this time; we need to perform a conversion.

From and Into

The Rust standard library defines two traits for infallible conversions: From and Into, in the std::convert module.

pub trait From<T>: Sized {
    fn from(value: T) -> Self;
}

pub trait Into<T>: Sized {
    fn into(self) -> T;
}

These trait definitions showcase a few concepts that we haven't seen before: supertraits and implicit trait bounds. Let's unpack those first.

Supertrait / Subtrait

The From: Sized syntax implies that From is a subtrait of Sized: any type that implements From must also implement Sized. Alternatively, you could say that Sized is a supertrait of From.

Implicit trait bounds

Every time you have a generic type parameter, the compiler implicitly assumes that it's Sized.

For example:

pub struct Foo<T> {
    inner: T,
}

is actually equivalent to:

pub struct Foo<T: Sized> 
{
    inner: T,
}

In the case of From<T>, the trait definition is equivalent to:

pub trait From<T: Sized>: Sized {
    fn from(value: T) -> Self;
}

In other words, both T and the type implementing From<T> must be Sized, even though the former bound is implicit.

Negative trait bounds

You can opt out of the implicit Sized bound with a negative trait bound:

pub struct Foo<T: ?Sized> {
    //            ^^^^^^^
    //            This is a negative trait bound
    inner: T,
}

This syntax reads as "T may or may not be Sized", and it allows you to bind T to a DST (e.g. Foo<str>). It is a special case, though: negative trait bounds are exclusive to Sized, you can't use them with other traits.

&str to String

In std's documentation you can see which std types implement the From trait.
You'll find that String implements From<&str> for String. Thus, we can write:

let title = String::from("A title");

We've been primarily using .into(), though.
If you check out the implementors of Into you won't find Into<String> for &str. What's going on?

From and Into are dual traits.
In particular, Into is implemented for any type that implements From using a blanket implementation:

impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

If a type U implements From<T>, then Into<U> for T is automatically implemented. That's why we can write let title = "A title".into();.

.into()

Every time you see .into(), you're witnessing a conversion between types.
What's the target type, though?

In most cases, the target type is either:

  • Specified by the signature of a function/method (e.g. Ticket::new in our example above)
  • Specified in the variable declaration with a type annotation (e.g. let title: String = "A title".into();)

.into() will work out of the box as long as the compiler can infer the target type from the context without ambiguity.