Variables and Mutability

Type Annotations

A type annotation explicitly states the data type of a variable, function parameter, return value, constant, or generic turbofish parameter. While Rust's type inference engine can deduce the types of most local variables inside a function body, explicit annotations are required wherever a declaration defines a boundary (such as function signatures and constants) or whenever multiple types could satisfy an expression.

Variable Type Annotation Syntax

In a let binding, a type annotation is written by placing a colon (:) and the type name immediately after the variable name (or destructuring pattern):

let mut count: u32 = 0;
    ─── ─────  ───   ─
     │    │     │    └── Initializer expression
     │    │     └─────── Explicit type annotation (`: u32`)
     │    └───────────── Variable identifier
     └────────────────── Optional mutability modifier
fn main() {
    let port: u16 = 8080;
    let ratio: f32 = 0.75;
    let label: &str = "production";
    let coordinates: (i64, i64) = (-12, 45);
    let bytes: [u8; 4] = [127, 0, 0, 1];

    println!("{label} on port {port} ({ratio}): {coordinates:?}, {bytes:?}");
}
production on port 8080 (0.75): (-12, 45), [127, 0, 0, 1]

When destructuring a tuple or struct with let, the type annotation applies to the entire pattern on the left-hand side of =, not to individual identifiers inside the pattern:

fn main() {
    let (host, port): (&str, u16) = ("localhost", 3000);
    println!("{host}:{port}");
}
localhost:3000

Writing let (host: &str, port: u16) = ... inside a let tuple pattern is a syntax error; annotate the whole tuple let (host, port): (&str, u16) = ... instead.

Where Type Annotations Are Required vs Optional

Construct Type Annotation Status Example
const item Always required const MAX_RETRIES: u32 = 3;
static item Always required static APP_NAME: &str = "cli";
Function parameter Always required fn scale(value: f64, factor: f64)
Function return type (non-()) Always required fn area(r: f64) -> f64
struct fields and enum payloads Always required struct User { id: u64 }
Local let variable Optional (unless ambiguous) let x = 5; or let x: i64 = 5;
Closure parameter and return type Optional (usually inferred from context) |x| x + 1 or |x: i32| -> i32 { x + 1 }

Resolving Ambiguous Return Types (E0284 / E0282)

Some standard library functions are return-type polymorphic—meaning their behavior depends on the type expected by the caller. The two most common examples are str::parse (which can parse a string into any type implementing FromStr, such as i32, u64, f64, or IpAddr) and Iterator::collect (which can build a Vec<T>, HashSet<T>, String, or BTreeMap<K, V>).

If you call .parse() or .collect() without telling the compiler which target type you want, compilation fails with E0284 or E0282 ("type annotations needed"):

// ❌ does not compile
fn main() {
    let number = "42".parse().unwrap();
    println!("{number}");
}
error[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let number = "42".parse().unwrap();
  |         ^^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `number` an explicit type
  |
2 |     let number: /* Type */ = "42".parse().unwrap();
  |               ++++++++++++

error: aborting due to 1 previous error

You can supply the required type in two ways:

  1. Annotating the let binding: let number: u32 = "42".parse().unwrap();
  2. Using turbofish syntax (::<Type>) on the method call: let number = "42".parse::<u32>().unwrap();
fn main() {
    // 1. Type annotation on the variable binding
    let parsed_a: u32 = "42".parse().unwrap();

    // 2. Turbofish `::<Type>` on the method call
    let parsed_b = "42".parse::<u32>().unwrap();

    let squares: Vec<i32> = (1..=4).map(|n| n * n).collect();

    println!("parsed_a = {parsed_a}, parsed_b = {parsed_b}, squares = {squares:?}");
}
parsed_a = 42, parsed_b = 42, squares = [1, 4, 9, 16]

Partial Type Annotations with Type Placeholders (_)

Inside a type annotation, the underscore _ acts as a type placeholder (or inferred type wildcard). It instructs the compiler to enforce the outer container type while inferring the inner element type automatically:

fn main() {
    // Enforce that the collection is a `Vec`, but let `rustc` infer the element type (`i32`)
    let numbers: Vec<_> = (1..=5).collect();

    // Enforce a 4-element array of `u64`
    let buffer: [u64; _] = [10, 20, 30, 40];

    // Parse a string while letting the error type in `Result<u16, _>` be inferred
    let port_result: Result<u16, _> = "8080".parse();

    println!("numbers: {numbers:?}");
    println!("buffer:  {buffer:?}");
    println!("port:    {port_result:?}");
}
numbers: [1, 2, 3, 4, 5]
buffer:  [10, 20, 30, 40]
port:    Ok(8080)

Note: The type placeholder _ is allowed only inside function bodies (such as let annotations and turbofish arguments). It is not allowed in function signatures, struct definitions, or const/static declarations (error[E0121]: the placeholder '_' is not allowed within types on item signatures).

Numeric Literal Suffixes vs Variable Annotations

For numeric primitives, a type annotation on the variable (let x: u64 = 100;) and a type suffix on the literal (let x = 100_u64;) achieve the same result:

fn main() {
    let annotated: i16 = -300;
    let suffixed = -300_i16;

    assert_eq!(annotated, suffixed);
    println!("Both are i16 with value {annotated}");
}
Both are i16 with value -300

Common Errors

Type Mismatch Between Annotation and Initializer (E0308)

When a type annotation is present, the compiler checks the initializer expression against that exact type and rejects any mismatch without performing implicit conversions:

// ❌ does not compile
fn main() {
    let count: u32 = -5;
    let message: String = "literal str slice";
}
error[E0600]: cannot apply unary operator `-` to type `u32`
 --> src/main.rs:2:22
  |
2 |     let count: u32 = -5;
  |                      ^^ cannot apply unary operator `-`
  |
  = note: unsigned values cannot be negated

error[E0308]: mismatched types
 --> src/main.rs:3:27
  |
3 |     let message: String = "literal str slice";
  |                  ------   ^^^^^^^^^^^^^^^^^^^ expected `String`, found `&str`
  |                  |
  |                  expected due to this
  |
help: try using a conversion method
  |
3 |     let message: String = "literal str slice".to_string();
  |                                              ++++++++++++

error: aborting due to 2 previous errors