Type Casts in Rust - The as Keyword

Understanding explicit type casts using the as keyword, including numeric conversions, pointer casts, and safety rules

The as keyword is Rust's built‑in mechanism for performing an explicit type cast when the compiler will not do it for you. It converts a value of one type into a value of another type — but only for a restricted set of type pairs that the language defines. Unlike many other languages, Rust has almost no implicit numeric conversions. An i32 never silently becomes an i64 or a f64. You tell the compiler exactly what you want with as.

Why Explicit Casts Exist

Languages that convert numbers automatically hide costs. Widening an integer is cheap, but narrowing one can lose information. Converting a float to an integer discards the fractional part, and out‑of‑range values cause real trouble. Rust makes these costs visible by requiring an as cast everywhere a type change happens. The keyword acts as a marker: “I understand the consequences, and I want this conversion anyway.”

No Implicit Conversions:

Rust’s refusal to perform implicit numeric casts is a deliberate design choice. An expression like let x: i32 = 5u8; will not compile. You must write let x: i32 = 5u8 as i32;. This forces you to think about precision loss and range mismatch before the code runs.

How the as Keyword Works

The syntax is simple:

value as TargetType

The compiler checks that a cast from the source type to the target type is allowed. If it is not — for example, casting a String to an i32 — you get a compile‑time error. When the cast is permitted, the compiler inserts the necessary low‑level instructions: truncation, bit‑pattern reinterpretation, sign extension, or pointer recalculation, depending on the types involved.

as is an expression. It produces a value of the target type, so you can use it anywhere an expression is expected.

Numeric Casts

Numeric casts are the most common use of as. The rules vary depending on whether the source and target are integers or floating‑point numbers.

Integer to Integer

Casting between integer types of different sizes or signedness follows bit‑level rules, not mathematical rounding or saturation.

  • Widening (smaller to larger): The value is sign‑extended if the source is signed and the target is also signed. If the source is unsigned, it is zero‑extended. No information is lost.
  • Narrowing (larger to smaller): The lower bits are kept; the higher bits are discarded. This can change the numeric value drastically, but it is well‑defined and never undefined behaviour.
let a: u32 = 0x1234_5678;
let b: u16 = a as u16;
println!("0x{:x}", b); // prints "0x5678"

The top 16 bits are thrown away. The result is always the original value modulo 2^N, where N is the bit width of the target type. For signed narrowing, the behaviour is the same — the compiler does not preserve the sign.

Silent Truncation:

Narrowing with as never panics, never saturates, and never warns at runtime. A value that is too large for the target type wraps around silently, which is a frequent source of logic bugs.

Floating‑Point to Integer

Converting a float to an integer truncates toward zero. The fractional part is dropped. This is safe as long as the truncated value fits in the integer’s range.

let x: f64 = 3.9;
let y: i32 = x as i32;
println!("{}", y); // prints 3

Out‑of‑Range Float‑to‑Int is Undefined Behaviour:

If the floating‑point value, after truncation, does not fit in the target integer type — for example, f64::MAX as i32 or f64::NEG_INFINITY as i32 — the result is undefined behaviour. The program may crash, produce garbage, or behave unpredictably. Always guard such casts with range checks or use safe conversion traits like TryFrom.

Integer to Floating‑Point

An integer becomes the nearest representable floating‑point number. Large integers may lose precision because a float cannot represent every integer beyond a certain magnitude, but the conversion is always safe.

Floating‑Point to Floating‑Point

Casting f64 to f32 rounds to the nearest representable value (with ties to even). Overflow results in infinity, and underflow in a subnormal or zero. Casting f32 to f64 is exact — every f32 value fits perfectly in f64.

Widening is Always Safe:

Any integer can be widened to a larger integer of the same signedness without data loss. Any f32 can be widened to f64 without loss. These casts are predictable and safe to use without additional checks.

Pointer and Integer Casts

as can convert between pointers and integers, and between different pointer types. These conversions are deeply tied to unsafe code.

  • Reference to raw pointer: &T can be cast to *const T, and &mut T to *mut T. This is allowed outside unsafe blocks because the operation itself does not dereference memory.
  • Raw pointer to raw pointer: Any pointer type can be cast to any other pointer type, including changing mutability, but this requires an unsafe block because you are circumventing the type system’s guarantees about what kind of data lives at that address.
  • Pointer to integer: *const T or *mut T can be cast to usize (or any integer type that fits the pointer width). This gives you the raw address as a number.
  • Integer to pointer: An integer can be cast to a raw pointer. This is always unsafe and is needed for memory‑mapped I/O or interacting with hardware.
let x: i32 = 42;
let ptr: *const i32 = &x as *const i32;
let addr: usize = ptr as usize;
let reconstructed: *const i32 = addr as *const i32;
// Dereferencing reconstructed would require unsafe.

Requires unsafe Context:

Creating a raw pointer from a reference does not require unsafe, but most uses of the resulting pointer (dereferencing, cast to a different pointee type) do. The as keyword itself does not change safety requirements; you still need an unsafe block to do anything dangerous with the pointer.

Enum and Boolean Casts

as can convert C‑like enums (enums without fields) and boolean values to integers.

Enum to Integer

A fieldless enum can be cast to any integer type. The variant’s discriminant value — the number assigned by the compiler (starting at 0 by default) — becomes the integer.

enum Color {
    Red,
    Green,
    Blue,
}
let n: u8 = Color::Green as u8;
println!("{}", n); // prints 1

You can also assign explicit discriminants, and the cast uses those values.

Boolean to Integer

true becomes 1, and false becomes 0 for any integer type.

let one: i32 = true as i32;
let zero: i32 = false as i32;

Character to Integer

A char can be cast to any integer type, giving the Unicode scalar value. If the value is too large for the target type, the high bits are truncated — this can produce a meaningless number, but it is not undefined behaviour.

let a: u8 = 'A' as u8;
println!("{}", a); // prints 65

What as Cannot Do

The as keyword only works for a fixed set of conversions defined by the language. It is not a general “convert anything” tool.

  • You cannot cast between types that are not related by the standard rules, such as String to i32 or a struct to another struct.
  • You cannot use as to cast array to slice (use &arr[..] instead).
  • You cannot cast a trait object to a concrete type with as.
  • as does not call From or Into implementations. It always follows the primitive coercion rules described above.

as is Not a Pattern Operator:

You cannot use as inside a match arm pattern. For example, EtherType::Arp as u16 =\u003e is illegal. Patterns and expressions are separate syntactic positions. To match on a discriminant value, store it in a constant with as first, or convert the matched integer back to the enum with a from_u16 function.

When to Use as vs. From and Into

as is low‑level. It operates on bits and raw pointers. For ordinary numeric conversions that you want to be safe and obvious, prefer the From and Into traits (i32::from(5u8)) or the fallible TryFrom and TryInto traits, which return a Result and force you to handle overflow. The standard library implements these traits for many conversions, and they guarantee a panic (or an error) instead of silent truncation or undefined behaviour.

let small: u8 = 200;
// let big: i32 = small as i32;             // fine, but no overflow protection
let big: i32 = i32::from(small);            // also fine, and communicates intent
let large: u16 = 1000;
// let bad: u8 = large as u8;               // truncates silently to 232
let checked: u8 = u8::try_from(large).expect("value too large");

The as keyword still has a place: raw pointer manipulation, performance‑critical numeric code where the wrapping semantics are exactly what you want, and casting between primitive types when you know the value is in range. For everything else, From/Into is the idiomatic Rust answer.

Summary and Next Steps

The as keyword gives you direct control over primitive conversions at the cost of requiring you to understand the underlying bit‑level behaviour. It is the only way to do certain casts — raw pointer arithmetic, enum discriminant extraction — but for everyday numeric work it is often the wrong tool. The real insight is that Rust provides a layered conversion story: as for raw, unchecked reinterpretation; From and Into for safe, infallible conversions; and TryFrom/TryInto for conversions that can fail. Which one you reach for says a great deal about the guarantees you want the compiler to enforce.