Sum Types — Enums
EssentialPrerequisites
A sum type lets a value be exactly one of a fixed set of alternatives. In Rust, sum types are expressed as enum.
Basic enums
The simplest enum is a list of named variants with no attached data:
enum Direction {
North,
South,
East,
West,
}
let heading = Direction::North;
This resembles enums in C or Zig: a named constant whose set of values is fixed and exhaustively known.
Enums with payloads
What distinguishes Rust enums from C-style enums is that each variant can carry data:
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
Triangle { base: f64, height: f64 }, // named fields
}
Each variant independently decides what data it carries — or none at all. Creating values:
let c = Shape::Circle(5.0);
let r = Shape::Rectangle(3.0, 4.0);
Matching on enums
The only way to access the data inside an enum variant is match. It is exhaustive: you must handle every variant:
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
If you add a new variant to Shape, every match without a catch-all will fail to compile — the compiler becomes a checklist. This exhaustiveness is the core reason to model domains with enums.
Why “sum”?
The value count of a sum type is the sum of its variants’ value counts. An enum with three unit variants has possible values. An Option<bool> has possible values: None, Some(true), or Some(false). This additive relationship gives the type family its name.
Option and Result
Two enums from the standard library appear throughout every Rust codebase:
enum Option<T> {
None,
Some(T),
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Option<T> represents a value that may or may not be present — Rust’s replacement for null. Result<T, E> represents a computation that may succeed with a T or fail with an E. Both force you to handle both cases explicitly.
fn parse_age(s: &str) -> Result<u8, String> {
s.parse::<u8>().map_err(|e| e.to_string())
}
match parse_age("42") {
Ok(age) => println!("age: {age}"),
Err(msg) => println!("error: {msg}"),
}
Making invalid states unrepresentable
Because an enum can only be in one state at a time, enums naturally model state machines. Consider a network connection:
enum Connection {
Connecting,
Connected { peer_addr: std::net::SocketAddr },
Disconnected { reason: String },
}
Storing the relevant data inside each variant means you cannot access the peer address unless you have first confirmed the connection is established — the type system prevents the invalid access. If the logic requires a peer_addr, the match arm that provides it is the only place that access compiles.
This pattern — making invalid states unrepresentable — is one of the most powerful uses of sum types in Rust.