Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust Types

This chapter is for exploring Rust values through let bindings. The point is not only to memorize types in isolation. The point is to see the actual shapes Rust uses when you write code.

A useful rule is:

let introduces a binding to a value, and the left-hand side may be a pattern, not only a single name.

That is why let is a good way to review Rust types. It is where simple values, references, tuples, structs, enums, and more advanced ownership forms all become concrete.

A broader pattern shows up again and again in Rust programs:

  • introduce bindings with let
  • shape those bindings into the right type and ownership form
  • use the APIs that belong to that exact form

A compact way to say it is:

Rust programs often bind values into explicit forms first, and then use the APIs that those forms make available.

That is why studying Rust through let bindings is useful. It is not only syntax review. It is also a way to see how type, ownership form, and available APIs line up inside real programs.

What let Is Doing

At its simplest:

#![allow(unused)]
fn main() {
let x = 5;
}

This means:

  • create a binding named x
  • infer its type from the right-hand side
  • bind that name to the value

So let is not mainly about mutation. It is about introducing a binding.

The Smallest Binding Forms

Plain binding

#![allow(unused)]
fn main() {
let x = 5;
}
  • binding: x
  • type: i32 by inference here
  • value form: owned value

Mutable binding

#![allow(unused)]
fn main() {
let mut x = 5;
x += 1;
}
  • same binding idea
  • the binding is mutable
  • this is about mutability of the binding, not a different type

Explicit type annotation

#![allow(unused)]
fn main() {
let x: i32 = 5;
}
  • same let
  • type written explicitly
  • useful when you want precision or clearer recall

Mutable binding with explicit type

#![allow(unused)]
fn main() {
let mut count: usize = 0;
}

This is the full basic shape:

  • let
  • optional mut
  • pattern or binding name
  • optional type annotation
  • initializer expression

let With Common Rust Value Types

Integer

#![allow(unused)]
fn main() {
let n: i32 = 10;
}

Boolean

#![allow(unused)]
fn main() {
let ready: bool = true;
}

Character

#![allow(unused)]
fn main() {
let ch: char = 'r';
}

String slice

#![allow(unused)]
fn main() {
let name: &str = "bobby";
}

Owned string

#![allow(unused)]
fn main() {
let name: String = String::from("bobby");
}

Vector

#![allow(unused)]
fn main() {
let nums: Vec<i32> = vec![1, 2, 3];
}

Hash map

#![allow(unused)]
fn main() {
use std::collections::HashMap;

let mut counts: HashMap<String, usize> = HashMap::new();
counts.insert("rust".to_string(), 1);
}

Option

#![allow(unused)]
fn main() {
let maybe_port: Option<u16> = Some(8080);
}

Result

#![allow(unused)]
fn main() {
let parsed: Result<u16, std::num::ParseIntError> = "8080".parse();
}

Box

#![allow(unused)]
fn main() {
let boxed: Box<i32> = Box::new(5);
}

Shared ownership

#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::sync::Arc;

let local_shared: Rc<String> = Rc::new("local".to_string());
let thread_shared: Arc<String> = Arc::new("thread-safe".to_string());
}
  • Rc<T> is for single-threaded shared ownership
  • Arc<T> is for thread-safe shared ownership
  • Arc<T> has a dedicated follow-up chapter: Rust Types: Arc

Synchronized shared state

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};

let counter: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
}

let With References

Shared reference

#![allow(unused)]
fn main() {
let s = String::from("rust");
let r: &String = &s;
}

Mutable reference

#![allow(unused)]
fn main() {
let mut s = String::from("rust");
let r: &mut String = &mut s;
}

Reference to a string slice

#![allow(unused)]
fn main() {
let s = String::from("rust");
let part: &str = &s[0..2];
}

These are important because let often introduces borrowed forms, not only owned forms.

let With Pattern Destructuring

This is where Rust becomes more expressive. The left-hand side of let may be a pattern.

Tuple destructuring

#![allow(unused)]
fn main() {
let pair: (i32, i32) = (19, 34);
let (x, y) = pair;
}

Struct destructuring

#![allow(unused)]
fn main() {
struct User {
    name: String,
    active: bool,
}

let user = User {
    name: "Bobby".to_string(),
    active: true,
};

let User { name, active } = user;
}

Tuple struct destructuring

#![allow(unused)]
fn main() {
struct Point(i32, i32);

let p = Point(3, 4);
let Point(x, y) = p;
}

Array destructuring

#![allow(unused)]
fn main() {
let arr = [10, 20, 30];
let [a, b, c] = arr;
}

Ignoring fields with _

#![allow(unused)]
fn main() {
let triple = (1, 2, 3);
let (x, _, z) = triple;
}

Ignoring the rest with ..

#![allow(unused)]
fn main() {
let nums = [1, 2, 3, 4];
let [first, ..] = nums;
}

let With Enums

let can destructure enum values when the pattern is irrefutable in that context. The most common enum-related forms in practice are if let and let else.

if let

#![allow(unused)]
fn main() {
let maybe = Some(42);

if let Some(value) = maybe {
    println!("{value}");
}
}

let else

#![allow(unused)]
fn main() {
let maybe = Some("rust");

let Some(name) = maybe else {
    return;
};

println!("{name}");
}

This is valuable in interviews because it shows that pattern matching is not only for match.

Shadowing

Rust also uses let to create a new binding with the same name. That is shadowing.

#![allow(unused)]
fn main() {
let x = 5;
let x = x + 1;
let x = x.to_string();
}

This means:

  • new binding each time
  • old binding no longer used after shadowing
  • the type may change across shadowing steps

That is why code like this is valid:

#![allow(unused)]
fn main() {
let (tx, rx) = std::sync::mpsc::channel::<u32>();
let rx = std::sync::Arc::new(std::sync::Mutex::new(rx));
}

The second rx is a new binding with a different type.

Highest-Yield let Forms To Recognize

These are the main let forms worth being able to explain quickly:

  1. let x = value;
  2. let mut x = value;
  3. let x: T = value;
  4. let mut x: T = value;
  5. let x = x + 1; for shadowing
  6. let (a, b) = tuple;
  7. let StructName { field, .. } = value;
  8. let [a, b, c] = array;
  9. let r = &value;
  10. let r = &mut value;
  11. let Some(x) = maybe else { ... };
  12. if let Some(x) = maybe { ... }

If you can explain those cleanly, you already cover a large percentage of the binding shapes that appear in normal Rust code.

Interview-Safe Summary

let introduces bindings, and in Rust those bindings may use patterns, not just variable names. That is why let naturally exposes Rust's type system: plain values, references, tuples, structs, enums, and ownership wrappers all show up through different binding shapes. A strong way to study Rust types is to study the forms that appear on the left and right side of let.