Skip to content

Data Types

A scalar type represents a single value. Rust has four primary scalar types.

Integer types encode their signedness and bit-width in the name.

SignedUnsignedWidth
i8u88-bit
i16u1616-bit
i32u3232-bit (default)
i64u6464-bit
i128u128128-bit
isizeusizepointer-width

The default integer type is i32. Use usize for indexing into arrays and slices.

Array and slice indices must be usize — not i32, not any other integer type. This is not a style preference; the compiler rejects any other index type outright.

// @expect-error E0277
fn main() {
let v: [i32; 3] = [10, 20, 30];
let i: i32 = 1;
println!("{}", v[i]);
}
error[E0277]: the type `[i32]` cannot be indexed by `i32`
--> src/main.rs:5:22
|
5 | println!("{}", v[i]);
| ^ slice indices are of type `usize` or ranges of `usize`
|
= help: the trait `SliceIndex<[i32]>` is not implemented for `i32`
= note: required for `[i32]` to implement `Index<i32>`
= note: required for `[i32; 3]` to implement `Index<i32>`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.

Fix it with v[i as usize]. usize is defined as “the same width as a pointer on this platform” precisely because it exists to index memory and measure sizes (.len() also returns usize) — it is not meant to represent quantities that could sensibly be negative, so Rust does not allow anything else to silently stand in for it.

Rust has f32 (single precision) and f64 (double precision, default). Always prefer f64 unless you have a specific memory or precision reason to use f32.

bool is either true or false. char represents a single Unicode scalar value and is four bytes wide — it can hold emoji, CJK characters, and any other Unicode point.

fn main() {
let flag: bool = true;
let letter: char = 'R';
let emoji: char = '\u{1F980}'; // crab emoji
println!("flag={}, letter={}, emoji={}", flag, letter, emoji);
}

A tuple groups values of potentially different types into one compound value. It has a fixed length. Access individual elements with dot notation and the zero-based index.

fn main() {
let point: (i32, f64, bool) = (1, 2.5, false);
println!("{} {} {}", point.0, point.1, point.2);
// Destructuring a tuple
let (a, b, c) = point;
println!("a={}, b={}, c={}", a, b, c);
}

An array holds a fixed number of values of the same type. The type annotation is [T; N] where T is the element type and N is the length — both known at compile time. Use slices (&[T]) when you need a dynamically-sized view.

fn main() {
let arr: [i32; 4] = [10, 20, 30, 40];
println!("first={}, last={}, len={}", arr[0], arr[3], arr.len());
// Repeat initializer: [value; count]
let zeros = [0_i32; 5];
println!("zeros len = {}", zeros.len());
}

Rust’s type system is static — every value has a type known at compile time — but you rarely need to write it out. The compiler infers types from context. You only need an annotation when the inference is ambiguous.

fn main() {
let n = 42; // inferred as i32
let f = 1.5; // inferred as f64
let s = "hello"; // inferred as &str
// Suffix syntax forces a specific numeric type
let byte = 255_u8;
println!("{} {} {} {}", n, f, s, byte);
}

Unlike C or JavaScript, Rust never silently widens or narrows a numeric type for you — not even from a smaller type to a strictly larger one that could hold every possible value. i32 and i64 cannot mix without an explicit conversion:

// @expect-error E0308
fn main() {
let a: i32 = 5;
let b: i64 = 10;
let c: i32 = b;
println!("{}", a + c);
}
error[E0308]: mismatched types
--> src/main.rs:5:18
|
5 | let c: i32 = b;
| --- ^ expected `i32`, found `i64`
| |
| expected due to this
|
help: you can convert an `i64` to an `i32` and panic if the converted value doesn't fit
|
5 | let c: i32 = b.try_into().unwrap();
| ++++++++++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.

Every numeric conversion in Rust is a decision you make explicitly, with as (silent truncation, covered next) or with TryFrom/TryInto (fallible, returns a Result so an out-of-range value is caught instead of truncated). There is no automatic promotion in either direction.

as converts between numeric types. Casting to a smaller integer type truncates: it keeps only the low-order bits, so a value that does not fit silently becomes a different number instead of erroring.

fn main() {
let big: i32 = 300;
let truncated = big as u8; // keeps only the low 8 bits
println!("truncated = {truncated}"); // 44, not 300
}

Plain arithmetic (+, -, *) is a different story: it does not truncate, it checks. In a debug build, an operation whose true result does not fit in the type panics instead of silently wrapping:

// @expect-panic
fn main() {
let x: u8 = std::hint::black_box(250);
let sum = x + 10; // 260 does not fit in a u8
println!("sum = {sum}");
}
thread 'main' (<id>) panicked at src/main.rs:4:15:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

A release build (cargo build --release) turns overflow checks off by default and silently wraps instead of panicking — relying on that difference is a bug waiting to surface only in production. Say what you mean instead with the explicit method families below, all defined for every integer type:

FamilyOn overflowReturn type
checked_add/checked_sub/checked_mulreturns None instead of panicking or wrappingOption<T>
wrapping_add/wrapping_sub/wrapping_mulwraps around on purpose (same bits as a release-mode +)T
saturating_add/saturating_sub/saturating_mulclamps to the type’s MIN/MAXT
overflowing_add/overflowing_sub/overflowing_mulwraps, and also tells you whether it did(T, bool)
fn main() {
let x: u8 = std::hint::black_box(250);
println!("wrapping = {}", x.wrapping_add(10)); // 4
println!("checked = {:?}", x.checked_add(10)); // None
println!("saturating = {}", x.saturating_add(10)); // 255
println!("overflowing = {:?}", x.overflowing_add(10)); // (4, true)
}
flowchart TD
  Q{"x + y might overflow -
which do you want?"}
  Q -->|"crash if it happens
(a bug, catch it)"| Debug["plain + in a debug build
(panics on overflow)"]
  Q -->|"treat it as absent"| Checked["checked_add -> Option::None"]
  Q -->|"wrap on purpose"| Wrapping["wrapping_add -> wraps"]
  Q -->|"clamp to the edge"| Saturating["saturating_add -> MAX/MIN"]
  Q -->|"wrap, but tell me if it did"| Overflowing["overflowing_add -> (value, bool)"]
Choosing an overflow strategy
fn main() {
// Scalar types
let n: i32 = -42;
let f: f64 = 3.14;
let b: bool = true;
let c: char = 'R';
println!("i32: {}, f64: {}, bool: {}, char: {}", n, f, b, c);
// Tuple: fixed-length, mixed types
let tup: (i32, f64, bool) = (1, 2.5, false);
println!("tuple.0 = {}, tuple.1 = {}, tuple.2 = {}", tup.0, tup.1, tup.2);
// Array: fixed-length, same type
let arr: [i32; 4] = [10, 20, 30, 40];
println!("arr[0] = {}, arr[3] = {}, len = {}", arr[0], arr[3], arr.len());
// Type inference
let inferred = 99_u8;
println!("inferred u8: {}", inferred);
}

Every number in Rust carries its width and signedness as part of its type, not as a runtime property you can query and branch on. That is why there is no implicit conversion: i32 and i64 are as different to the type checker as i32 and bool are — they simply are not the same type, even though one can always represent every value the other can. Casting with as is you telling the compiler “I know this might lose information, do it anyway”; the checked_/wrapping_/saturating_/overflowing_ families are you telling it exactly which of four defined behaviors you want when arithmetic — not a cast — produces a value that does not fit.

Requiring an explicit usize for indexing and an explicit conversion between i32 and i64 costs a few extra characters at nearly every array access and cross-type calculation. In exchange, a whole category of “it worked on my machine because the numbers happened to be small” bugs cannot compile — the type checker forces you to decide, at the point of conversion, whether an out-of-range value should panic, wrap, saturate, or be reported back as None, instead of that decision being made implicitly (and inconsistently) by whatever the CPU happens to do.

  • “as is a safe, general-purpose conversion.” as between integer types truncates silently on narrowing — it will never panic, which means it is exactly the wrong tool when an out-of-range value indicates a bug you want to catch. Prefer try_into() (which returns a Result) whenever the input could plausibly be out of range.
  • “Debug and release builds behave the same way, just faster in release.” Integer overflow is one concrete counterexample: a debug build panics on overflow, a release build wraps silently by default. Code that “works” only because you always test in debug mode can misbehave the moment it ships in release.
  • “usize is just u32 on 64-bit machines, so I can use them interchangeably.” usize is defined as pointer-width — 64 bits on today’s common desktop/server targets, but the compiler still treats it as a distinct type from u32 and u64, and it can be 32 bits or 16 bits on other targets. Casting between them still needs as or try_into() regardless of which platform you are compiling for.

AI-generated code frequently indexes a Vec or array with a loop variable declared as i32 (a habit carried over from C-like languages), then adds an as usize cast at the point of use instead of declaring the loop variable as usize to begin with — or worse, casts a value that could be negative straight to usize, silently turning it into a huge positive number instead of a panic.

🐛 Spot the bug

fn get_last_n(items: &[i32], n: i32) -> &[i32] {
let start = items.len() as i32 - n;
&items[start as usize..]
}
Show the answer

If n is larger than items.len(), items.len() as i32 - n goes negative — and casting a negative i32 to usize does not panic, it reinterprets the bits as a huge positive number (-1i32 as usize is usize::MAX on a 64-bit target). The subsequent slice access then panics far from the real bug, with a confusing “range end index out of range for slice” instead of a clear message about n being too large. The fix is to keep the length arithmetic in usize and use checked_sub to catch the underflow explicitly: let start = items.len().saturating_sub(n as usize);.

What is the default integer type when you write let x = 42?
How wide is a char value in Rust?
Which type annotation describes an array of five i32 values?
What does the suffix in 255_u8 communicate to the compiler?
Does this compile?
fn main() {
    let v: [i32; 3] = [10, 20, 30];
    let i: i32 = 1;
    println!("{}", v[i]);
}
What does this print?
fn main() {
    let x: u8 = 250;
    println!("{:?}", x.checked_add(10));
    println!("{}", x.saturating_add(10));
}