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
- Explicit `as` casts and how they truncate or wrap
- The From trait and the free Into it provides
- TryFrom / TryInto for conversions that return a Result
- Parsing strings with parse::<T>()
- Producing strings with to_string and ToString
- Why Rust has no implicit numeric coercion
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 = 7Mini-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)
- ❌ "mismatched types: expected i64, found i32" — Rust won't auto-widen. ✅ Convert explicitly with i64::from(x) or x as i64.
- ❌ "type annotations needed" on parse — parse can't tell the target type. ✅ Use a turbofish "42".parse::<i32>() or annotate the binding.
- ❌ Surprising value from as — A narrowing cast wrapped or truncated. ✅ Use TryFrom when overflow should be an error, not silent.
- ❌ unwrap() panicked on bad input — The Result was Err. ✅ Handle it with match, unwrap_or, or ?.
Pro Tips
- 💡 Default to From/Into for safe conversions; reach for as only in low-level numeric code.
- 💡 Implement From, not Into for your own types — you get the matching Into for free.
- 💡 Round before casting: if you want rounding, call x.round() as i32 rather than relying on truncation.
📋 Quick Reference — Conversions
| Tool | Use when |
|---|---|
| x as T | Numeric 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!
- ✅ as always succeeds but can truncate or wrap — use it deliberately
- ✅ From gives you Into for free; both are infallible
- ✅ TryFrom/TryInto and parse return a Result for fallible conversions
- ✅ Rust never coerces numbers implicitly — every conversion is visible
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
- Previous: Pattern Matching (match, if let, while let)
- Next: Constants & Statics — const vs static, const fn, and compile-time evaluation
- Quick reference: Rust cheat sheet
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.