Data Types
Scalar Types
Section titled “Scalar Types”A scalar type represents a single value. Rust has four primary scalar types.
Integers
Section titled “Integers”Integer types encode their signedness and bit-width in the name.
| Signed | Unsigned | Width |
|---|---|---|
i8 | u8 | 8-bit |
i16 | u16 | 16-bit |
i32 | u32 | 32-bit (default) |
i64 | u64 | 64-bit |
i128 | u128 | 128-bit |
isize | usize | pointer-width |
The default integer type is i32. Use usize for indexing into arrays and slices.
usize Is Not Optional for Indexing
Section titled “usize Is Not Optional for Indexing”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 E0277fn 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.
Floating-Point Numbers
Section titled “Floating-Point Numbers”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.
Booleans and Characters
Section titled “Booleans and Characters”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);}Compound Types
Section titled “Compound Types”Tuples
Section titled “Tuples”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);}Arrays
Section titled “Arrays”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());}Type Inference
Section titled “Type Inference”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);}No Implicit Numeric Conversion
Section titled “No Implicit Numeric Conversion”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 E0308fn 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.
Casting with as, Truncation, and Overflow
Section titled “Casting with as, Truncation, and Overflow”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-panicfn 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 overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceA 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:
| Family | On overflow | Return type |
|---|---|---|
checked_add/checked_sub/checked_mul | returns None instead of panicking or wrapping | Option<T> |
wrapping_add/wrapping_sub/wrapping_mul | wraps around on purpose (same bits as a release-mode +) | T |
saturating_add/saturating_sub/saturating_mul | clamps to the type’s MIN/MAX | T |
overflowing_add/overflowing_sub/overflowing_mul | wraps, 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)"] 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);}Compiling…
Mental Model
Section titled “Mental Model”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.
Trade-offs
Section titled “Trade-offs”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.
Common Misconceptions
Section titled “Common Misconceptions”- “
asis a safe, general-purpose conversion.”asbetween 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. Prefertry_into()(which returns aResult) 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.
- “
usizeis justu32on 64-bit machines, so I can use them interchangeably.”usizeis defined as pointer-width — 64 bits on today’s common desktop/server targets, but the compiler still treats it as a distinct type fromu32andu64, and it can be 32 bits or 16 bits on other targets. Casting between them still needsasortry_into()regardless of which platform you are compiling for.
Reading AI-generated code
Section titled “Reading AI-generated code”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);.