Rust Data Types

Numeric Type Inference

When you write an unsuffixed numeric literal in Rust—such as 42 or 3.14—it does not immediately lock into a single concrete type. Instead, rustc creates a temporary type variable ({integer} for integer literals, {float} for floating-point literals) and resolves it to a concrete numeric type by inspecting how the variable is used later in the function, falling back to i32 or f64 only if no external constraint appears.

How {integer} and {float} Unification Works

let x = 42;       <-- Step 1: `x` is assigned temporary type `{integer}`
  │
  ├── Case A: `x` is later passed to `Vec::with_capacity(x)` (which expects `usize`)
  │           ==> `{integer}` unifies with `usize`! (`x` has type `usize` from line 1)
  │
  └── Case B: `x` is only printed or used in unconstrained arithmetic (`x + 1`)
              ==> `{integer}` falls back to the default integer type `i32`!
Literal Kind Compiler Placeholder Default Fallback Type (If Unconstrained) Possible Concrete Types It Can Infer To
Unsuffixed integer (42, 0xFF, 0b1010) {integer} i32 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
Unsuffixed float (3.14, 1.0e-5) {float} f64 f32, f64
Suffixed literal (42_u64, 3.14_f32) Concrete immediately N/A Locked to u64 or f32 at the literal site

Contextual Clues That Drive Numeric Inference

The compiler's Hindley-Milner type inference engine gathers constraints across the entire function body—even from statements many lines below the let declaration:

fn require_u16(port: u16) {
    println!("Port (u16): {port}");
}

fn main() {
    // 1. Inferred from a later function call:
    let port = 8080; // `{integer}` -> resolved to `u16` because of `require_u16(port)` below!
    require_u16(port);

    // 2. Inferred from slice/array indexing (which always requires `usize`):
    let data = ["alpha", "beta", "gamma"];
    let idx = 1; // `{integer}` -> resolved to `usize` because of `data[idx]`!
    println!("Element at idx: {}", data[idx]);

    // 3. Inferred from binary arithmetic with a known type:
    let explicit_u64: u64 = 1000;
    let delta = 250; // `{integer}` -> resolved to `u64` because of `explicit_u64 + delta`!
    println!("Sum (u64): {}", explicit_u64 + delta);

    // 4. Unconstrained literal falls back to `i32` (for integer) and `f64` (for float):
    let default_int = 10;
    let default_float = 2.5;
    println!("Defaults: {default_int} (i32), {default_float} (f64)");
}
Port (u16): 8080
Element at idx: beta
Sum (u64): 1250
Defaults: 10 (i32), 2.5 (f64)

Container Element Inference (Vec::new() + .push())

When you create an empty collection like let mut list = Vec::new(); and later push an integer or float literal into it, both the collection's element type and the literal's numeric type unify simultaneously:

fn main() {
    let mut bytes = Vec::new(); // Type starts as `Vec<?T>`
    bytes.push(255_u8);         // `?T` unifies with `u8` -> `bytes` is `Vec<u8>`
    bytes.push(42);             // Unsuffixed `42` (`{integer}`) is inferred as `u8`!

    println!("bytes: {bytes:?}");
}
bytes: [255, 42]

Conflicting Numeric Constraints (E0308)

A single variable can only ever have one concrete type. Once an {integer} variable x is unified with u16 (for example, by passing it to a function expecting u16), attempting to also use x where a u32 or usize is expected causes a compile-time type mismatch:

// ❌ does not compile
fn takes_u16(n: u16) {
    println!("{n}");
}

fn takes_u32(n: u32) {
    println!("{n}");
}

fn main() {
    let x = 100;
    takes_u16(x); // Locks `x` to type `u16`!
    takes_u32(x); // Error: expected `u32`, found `u16`!
}
error[E0308]: mismatched types
  --> src/main.rs:12:15
   |
11 |     takes_u16(x); // Locks `x` to type `u16`!
   |     --------- - here the type of `x` is inferred to be `u16`
12 |     takes_u32(x); // Error: expected `u32`, found `u16`!
   |     --------- ^ expected `u32`, found `u16`
   |     |
   |     arguments to this function are incorrect
   |
help: you can convert a `u16` to a `u32`
   |
12 |     takes_u32(x.into());
   |                +++++++

error: aborting due to 1 previous error

Out-of-Range Literals (overflowing_literals)

When an {integer} literal is unified with a specific integer type (or defaults to i32), the compiler checks that the literal value fits inside that type's [MIN..=MAX] range:

  • If you write let big = 5_000_000_000; with no type constraint, {integer} defaults to i32 (MAX = 2_147_483_647) and triggers an overflowing_literals error advising you to add : i64 or : u64.
  • If you write let byte: u8 = 300;, {integer} unifies with u8 (0..=255) and fails to compile:
// ❌ does not compile
fn main() {
    let byte: u8 = 300;
}
error: literal out of range for `u8`
 --> src/main.rs:2:20
  |
2 |     let byte: u8 = 300;
  |                    ^^^
  |
  = note: the literal `300` does not fit into the type `u8` whose range is `0..=255`
  = note: `#[deny(overflowing_literals)]` on by default

error: aborting due to 1 previous error

Ambiguous Numeric Method Calls (E0689)

Calling an inherent method (such as .abs(), .pow(2), or .sqrt()) directly on an unsuffixed numeric literal or unconstrained {integer} / {float} variable before its concrete width is known fails with E0689 because the compiler refuses to guess whether you want f32::sqrt or f64::sqrt:

// ❌ does not compile
fn main() {
    let x = 16.0;
    println!("{}", x.sqrt());
}
error[E0689]: can't call method `sqrt` on ambiguous numeric type `{float}`
 --> src/main.rs:3:22
  |
2 |     let x = 16.0;
  |         - you must specify a type for this binding, like `f64`
3 |     println!("{}", x.sqrt());
  |                      ^^^^
  |
help: you must specify a concrete type for this numeric value, like `f64`
  |
2 |     let x: f64 = 16.0;
  |          +++++

error: aborting due to 1 previous error

Specify either a variable type annotation (let x: f64 = 16.0;) or a literal suffix (16.0_f64.sqrt()) whenever calling inherent numeric methods.