Type Conversions (From, Into, as, TryFrom)

Reviewed & published by Brayan K

Type conversion is how you turn a value of one type into another in Rust — always explicitly, using as casts, the From/Into traits for infallible changes, and TryFrom/TryInto for ones that can fail.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll learn as casts and their truncation, the From/Into pair, fallible TryFrom with Result, parse, and why Rust never coerces implicitly.

What You'll Learn in This Lesson

1️⃣ The `as` Cast (and Its Pitfalls)

The as keyword is a low-level numeric cast that always compiles and always succeeds — which is exactly why it's risky. Casting a float to an integer truncates toward zero, and casting to a smaller integer type can wrap the bits and lose information.

fn main() {
    // `as` performs an explicit numeric cast. Rust has NO implicit coercion.
    let pi = 3.99_f64;
    let truncated = pi as i32;       // float -> int TRUNCATES (no rounding)
    println!("3.99 as i32 = {}", truncated);

    // Casting to a smaller integer can LOSE information (wraps the bits).
    let big = 300_i32;
    let small = big as u8;           // 300 doesn't fit in u8 (0..=255)
    println!("300 as u8 = {}", small);

    // int -> float is lossless for small values.
    let n = 7_i32;
    println!("7 as f64 = {}", n as f64 / 2.0);

    // char -> u32 reads the Unicode code point.
    let ch = 'A';
    println!("'A' as u32 = {}", ch as u32);
}

2️⃣ From & Into: Infallible Conversions

For conversions that can never fail, implement the From trait. The payoff is automatic: implementing From<A> for B gives you Into for free, so a.into() just works. The standard library uses this pattern everywhere, from String::from to integer widening.

// Implementing From gives you Into for free, automatically.
struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

fn main() {
    let c = Celsius(100.0);

    // Use From explicitly...
    let f1 = Fahrenheit::from(Celsius(0.0));
    println!("0C = {}F", f1.0);

    // ...or Into, which you got for free.
    let f2: Fahrenheit = c.into();
    println!("100C = {}F", f2.0);

    // The standard library uses From everywhere, e.g. String::from(&str).
    let s = String::from("hi");
    let n: i64 = i64::from(42_i32);   // widening is infallible -> From
    println!("{} {}", s, n);
}

Notice you only wrote impl From<Celsius> for Fahrenheit, yet c.into() compiled. That's the blanket implementation in the standard library handing you Into automatically.

3️⃣ TryFrom, parse, and to_string

When a conversion might fail, use TryFrom/TryInto, which return a Result. The same idea powers str::parse, which turns text into a number and reports invalid input. Going the other way, to_string renders any Display value as a String.

fn main() {
    // TryFrom/TryInto are for conversions that MIGHT fail -> they return Result.
    // Match on the Result rather than printing it with {:?}. The Debug form of
    // the error is an internal detail that std has changed between releases
    // (1.94 printed TryFromIntError(()), 1.98 prints TryFromIntError(PosOverflow)),
    // while its Display message below is documented and stable.
    for value in [200_i32, 300_i32] {
        match u8::try_from(value) {
            Ok(v)  => println!("{} -> fits in a u8: {}", value, v),
            Err(e) => println!("{} -> {}", value, e),
        }
    }

    // parse() turns a &str into a number, returning Result.
    let n: i32 = "42".parse().unwrap();
    println!("parsed = {}", n);

    // Handle a bad parse gracefully with match.
    match "xyz".parse::<i32>() {
        Ok(v) => println!("got {}", v),
        Err(_) => println!("not a number"),
    }

    // to_string() / ToString go the other way: value -> String.
    let text = 255.to_string();
    println!("as string: {}", text);
}

Your turn. Fill in the blanks marked ___, then run it.

// 🎯 YOUR TURN — fill in the blanks, then run it.
fn main() {
    let x = 9.7_f64;
    // 1) Explicitly cast the float to an i32 (it truncates to 9).
    let n = x ___ i32;
    println!("9.7 as i32 = {}", n);

    // 2) Parse a string into an i32 (the type annotation guides parse).
    let parsed: i32 = "256".___().unwrap();
    println!("parsed = {}", parsed);

    // 3) Widen an i32 into an i64 with the infallible From conversion.
    let widened = i64::___(7_i32);
    println!("widened = {}", widened);
}

// ✅ Expected output:
//    9.7 as i32 = 9
//    parsed = 256
//    widened = 7

Mini-Challenge: A Forgiving Parser

Parse a mix of valid and invalid strings, sum the good ones, and count the failures — handling each Result gracefully. Run it with cargo run and check the output.

// 🎯 MINI-CHALLENGE: a forgiving number parser.
//
// Given the array ["10", "20", "oops", "30"]:
// 1. Try to parse each string into an i32 with .parse::<i32>().
// 2. On Ok, add it to a running sum; on Err, increment a failure counter.
// 3. Print the sum, the failure count, and sum cast to f64 divided by 2.0.
//
// ✅ Example output:
//    sum = 60
//    failures = 1
//    sum as f64 / 2.0 = 30

fn main() {
    let inputs = ["10", "20", "oops", "30"];
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Conversions

ToolUse when
x as TNumeric cast; may truncate/wrap
T::from(x)Infallible conversion
x.into()Infallible, target-inferred
T::try_from(x)May fail; returns Result
s.parse::<T>()str -> number (Result)
x.to_string()value -> String

🎉 Lesson Complete!

Practice quiz

What does 3.99_f64 as i32 evaluate to?

  • 4
  • 5
  • 3
  • 0

Answer: 3. Casting a float to an integer truncates toward zero, so 3.99 becomes 3.

What does 300_i32 as u8 produce?

  • 44
  • 300
  • 255
  • 0

Answer: 44. 300 does not fit in a u8, so the bits wrap and the result is 44.

Implementing From<A> for B automatically gives you what?

  • TryFrom for free
  • Display for free
  • Clone for free
  • Into for free

Answer: Into for free. A blanket impl in std means implementing From gives the matching Into automatically.

Which trait is for conversions that MIGHT fail?

  • From
  • TryFrom
  • Into
  • AsRef

Answer: TryFrom. TryFrom and TryInto return a Result so failure is explicit.

What does u8::try_from(300_i32) return?

  • Err(...)
  • Ok(44)
  • Ok(300)
  • None

Answer: Err(...). 300 does not fit in a u8, so try_from returns an Err instead of wrapping.

How do you convert a &str into a number?

  • Call .into()
  • Use as i32
  • Call .parse()
  • Call .to_string()

Answer: Call .parse(). .parse() returns a Result because the text might not be a valid number.

What does "42".parse::<i32>() return on success?

  • 42
  • Ok(42)
  • Some(42)
  • Err(42)

Answer: Ok(42). parse returns a Result, so a valid parse yields Ok(42).

Why does Rust have no implicit numeric coercion?

  • Because it is slow
  • Because of the borrow checker
  • It actually does coerce
  • To keep every conversion visible and intentional

Answer: To keep every conversion visible and intentional. Requiring explicit conversions avoids silent precision loss and surprising types.

Which is infallible and lossless for widening an i32 to i64?

  • x as u8
  • i64::from(x)
  • i64::try_from(x)
  • x.parse()

Answer: i64::from(x). Widening always succeeds, so From (i64::from) models it with no Result needed.

How do you turn a number like 255 into a String?

  • 255 as String
  • String::parse(255)
  • 255.to_string()
  • 255.into_str()

Answer: 255.to_string(). .to_string() renders any Display value as a String.

Continue this course

Frequently asked questions

What's the difference between `as` and From/Into?

The as keyword is a primitive numeric (and pointer) cast that always succeeds but can silently lose information — casting 3.99 as i32 truncates to 3, and 300 as u8 wraps to 44 because the value doesn't fit. From and Into are trait-based conversions that are guaranteed lossless and infallible: i64::from(x) widens an i32 with no surprises. Prefer From/Into when a conversion can never fail, and reserve as for low-level numeric work where you genuinely want truncating or wrapping behavior and have thought about the consequences.

When do I use TryFrom instead of From?

Use TryFrom (and its mirror TryInto) when a conversion can fail. From models conversions that always succeed, like widening an integer, whereas TryFrom returns a Result so the caller can handle the failure case. The classic example is narrowing: u8::try_from(300) returns an Err because 300 doesn't fit in a u8, instead of silently wrapping the way 300 as u8 would. Reaching for TryFrom makes the possibility of failure explicit in the type signature and forces you to deal with it.

How do I convert a string to a number and back?

To go from text to a number, call .parse() on a &str, which returns a Result because the text might not be valid. You usually tell it the target type with a turbofish, like "42".parse::<i32>(), or via a type annotation on the binding. Handle the Result with match, unwrap_or, or the ? operator rather than blindly unwrapping. To go the other way, from a number to a String, call .to_string() or use format!("{}", n). Both rely on standard traits (FromStr and Display/ToString respectively).

Why doesn't Rust convert between numeric types automatically?

Rust has no implicit numeric coercion on purpose. Languages that auto-convert can silently lose precision, wrap values, or pick a surprising type, and those bugs are hard to spot. By requiring you to write the conversion — an as cast, an .into(), or a try_into() — Rust makes every change of type visible in the source, so the cost and the risk are right there to see. It feels verbose at first, but it eliminates a whole category of subtle arithmetic bugs and makes code easier to audit.