Skip to content

Enums as Algebraic Data Types

TypeScript unions are structural — Rust enums carry data

Section titled “TypeScript unions are structural — Rust enums carry data”

TypeScript has union types ("circle" | "rectangle") and discriminated unions (objects with a kind field). These are powerful, but they are a convention — the compiler trusts you to set the discriminant correctly.

Rust enums are algebraic data types (ADTs). Each variant can carry its own data — different types per variant. The compiler enforces exhaustive pattern matching: if you add a new variant, every match in your codebase that handles the enum must be updated or the code will not compile.

TypeScript
// TypeScript — discriminated union (convention-based)
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "rectangle": return shape.width * shape.height;
case "triangle": return 0.5 * shape.base * shape.height;
// If you add a new variant and forget a case, TypeScript
// will warn only if you use a never-check pattern.
}
}
Rust
#[derive(Debug)]
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle { base: f64, height: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
}
fn main() {
let shapes = vec![
Shape::Circle(3.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle { base: 6.0, height: 4.0 },
];
for s in &shapes {
println!("{:?} => area = {:.2}", s, s.area());
}
}

Enums are the idiomatic way to model state machines in Rust. Each state variant carries only the data relevant to that state:

TypeScript
// TypeScript — state machine with discriminated union
type ConnectionState =
| { status: "disconnected" }
| { status: "connecting"; attempt: number }
| { status: "connected"; sessionId: string };
function describe(state: ConnectionState): string {
switch (state.status) {
case "disconnected": return "not connected";
case "connecting": return `attempt ${state.attempt}`;
case "connected": return `session ${state.sessionId}`;
}
}
Rust
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
}
fn describe(state: &ConnectionState) -> String {
match state {
ConnectionState::Disconnected => "not connected".to_string(),
ConnectionState::Connecting { attempt } => format!("attempt {}", attempt),
ConnectionState::Connected { session_id } => format!("session {}", session_id),
}
}
fn main() {
let states = vec![
ConnectionState::Disconnected,
ConnectionState::Connecting { attempt: 3 },
ConnectionState::Connected { session_id: "abc-123".to_string() },
];
for s in &states {
println!("{}", describe(s));
}
}
#[derive(Debug)]
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle { base: f64, height: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
}
fn main() {
let shapes = vec![
Shape::Circle(3.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle { base: 6.0, height: 4.0 },
];
for s in &shapes {
println!("{:?} => area = {:.2}", s, s.area());
}
}
What happens in Rust if your `match` does not cover all enum variants?
What is an algebraic data type (ADT) feature that Rust enums have but TypeScript enums do NOT?
In a Rust `match`, what does the `_` arm do?