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:
letintroduces 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:
i32by 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 ownershipArc<T>is for thread-safe shared ownershipArc<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:
let x = value;let mut x = value;let x: T = value;let mut x: T = value;let x = x + 1;for shadowinglet (a, b) = tuple;let StructName { field, .. } = value;let [a, b, c] = array;let r = &value;let r = &mut value;let Some(x) = maybe else { ... };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
letintroduces bindings, and in Rust those bindings may use patterns, not just variable names. That is whyletnaturally 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 oflet.