Type System Fundamentals

Static Type Checking

Static type checking is the compile-time verification phase in which rustc proves that every expression, variable binding, function call, and trait bound in a program obeys the rules of its type before any machine code is emitted.

Properties of Rust's Static Type System

Property How Rust Enforces It What It Prevents at Compile Time
Soundness (Strong typing) No implicit lossy numeric conversions (i32 to u32 or f64), no arbitrary pointer casts in safe code Silent truncation, sign-extension bugs, type confusion
Exhaustive sum-type checking Every match on an enum, Option<T>, or Result<T, E> must cover all variants Unhandled null dereferences (NullPointerException), forgotten states
Affine / Ownership typing Non-Copy values can be moved at most once (E0382) Use-after-free, double-free
Region / Lifetime typing Every reference &'a T and &'a mut T carries a compile-time lifetime 'a Dangling pointers, data races, iterator invalidation
Zero runtime type tags Concrete types are erased after monomorphization (unless dyn Any / dyn Trait is requested explicitly) Per-object boxing and runtime type-check overhead

Compile-Time Rejection of Type Mismatches across Control-Flow Branches (E0308)

In Rust, if / else and match are expressions whose branches must evaluate to the exact same type. If two branches return different types, compilation fails immediately with E0308:

// ❌ does not compile
fn select_timeout(fast: bool) -> u64 {
    if fast {
        100_u64
    } else {
        "default_timeout"
    }
}

fn main() {}
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:6:9
  |
3 | /     if fast {
4 | |         100_u64
  | |         ------- expected because of this
5 | |     } else {
6 | |         "default_timeout"
  | |         ^^^^^^^^^^^^^^^^^ expected `u64`, found `&str`
7 | |     }
  | |_____- `if` and `else` have incompatible types

Type-Directed Memory Layout and Dispatch

Because the compiler knows the exact static type of every local variable at compile time, it determines:

  1. Exact stack frame offsets and register allocation (size_of::<T>() and align_of::<T>()).
  2. Which trait implementation to invoke (resolving a + b or x.cmp(&y) to direct machine instructions).
  3. Where destructors (Drop::drop) must be inserted at scope exits and unwind paths.
fn clamp_u16(value: u16, min: u16, max: u16) -> u16 {
    if value < min {
        min
    } else if value > max {
        max
    } else {
        value
    }
}

fn main() {
    let port = clamp_u16(80, 1024, 65535);
    println!("Clamped port: {port}");
}
Clamped port: 1024