Type System Fundamentals

Type Inference

Type inference in Rust (based on a bidirectional Hindley–Milner constraint solver) automatically deduces the concrete types of local variables, generic type arguments, and closure parameters from how values are initialized, passed, and returned within a function body.

Boundary Rule: Explicit Signatures vs. Inferred Bodies

Rust draws a deliberate boundary between function/item signatures and function bodies:

  • Function, method, const, and static signatures require explicit type annotations. This guarantees that changing a function's body never silently breaks downstream callers, enables modular per-function type checking, and makes API documentation self-contained.
  • Inside a function body, let bindings, closures, and generic calls infer their types forward from initializers and backward from how the variable is used later in the function!
fn parse_and_collect(raw: &str) -> Vec<u32>   <── Explicit types at the function boundary
{
    let mut out = Vec::new();                 <── Inferred as `Vec<u32>` from return type!
    for token in raw.split(',') {             <── `token` inferred as `&str`
        if let Ok(n) = token.trim().parse() { <── `parse::<u32>()` inferred from `out.push(n)`!
            out.push(n);
        }
    }
    out
}
fn parse_and_collect(raw: &str) -> Vec<u32> {
    let mut out = Vec::new();
    for token in raw.split(',') {
        if let Ok(n) = token.trim().parse() {
            out.push(n);
        }
    }
    out
}

fn main() {
    let values = parse_and_collect("10, 25, invalid, 40");
    println!("Parsed values: {values:?}");
}
Parsed values: [10, 25, 40]

Partial Type Hints with the _ Type Placeholder and Turbofish (::<T>)

When the compiler cannot uniquely determine a type from context (E0282), you do not need to spell out the entire nested type. Use either:

  1. The _ type placeholder (let v: Vec<_> = ...;) to specify the outer container while letting the compiler infer the inner element type.
  2. Turbofish syntax (method::<Type>()) on a generic function or method call (iter.collect::<Vec<_>>(), "42".parse::<i64>()).
use std::collections::BTreeMap;

fn main() {
    let pairs = [("http", 80_u16), ("https", 443)];

    // Specifies the container `BTreeMap` while `_` infers `&str` and `u16`:
    let ports: BTreeMap<_, _> = pairs.into_iter().collect();

    println!("Ports: {ports:?}");
}
Ports: {"http": 80, "https": 443}

Default Numeric Fallback (i32 and f64)

If an integer literal (let x = 42;) or floating-point literal (let y = 3.14;) is unconstrained by any surrounding type expectation, the type checker defaults unconstrained integer inference variables to i32 and unconstrained floating-point inference variables to f64.