Language Internals
Part 4 of 11 · Rust Language ProficiencyTypes, Structs, Enums & Pattern Matching
Structs, enums, pattern matching, Option as enum.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
Struct or enum?
Answer
Struct when every field is present together. Enum when the value is one of several shapes.
L2
Why prefer an enum to a pair of bool flags?
Answer
Illegal combinations become unrepresentable. Two bools allow all four states.
L3
What does match require?
Answer
Every variant, or a wildcard you can defend. The compiler checks exhaustiveness.
L4
if let or match?
Answer
if let when you care about one pattern. match when every shape needs an arm.
L5
What is Option?
Answer
An enum with Some and None. Methods like map replace a null check.
L6
How does a Rust struct differ from a class?
Answer
impl blocks add methods. There is no inheritance tree by default. The struct owns its fields.
L7
Where can a match still be wrong?
Answer
A wildcard that hides a new variant you should have handled. Prefer naming the arms.
Failure modes
Bool flag combinations
Two flags encode a state the domain does not have, and every caller re-checks the pair.
Wildcard match
A new variant compiles and takes the wrong arm.
unwrap on Option
A library turns absence into a panic.
Stringly modes
A status string replaces an enum, so typos are data.
Misconceptions
Rust enums are like C enums of integers.
Variants can carry data. That is the sum type, closer to a TypeScript discriminated union.
Option is a nullable pointer.
It is an enum. The caller matches or maps it.
Structs inherit.
Reuse goes through traits, on the next page, not a base class.
Interviewer traps
Modeling a state machine as optional fields on one struct.
Put each state in an enum variant so the other fields cannot appear.
Saying TypeScript switch is exhaustive by default.
You need a discriminant and a never check. Rust match is checked on the enum.
Design scenario
Same prompt for every reader.
Requirements
One enum, data on the variants that need it, and a match with no wildcard.
Traffic / scale
Many small messages, one shape each.
Latency
Matching is a branch, not a class allocation.
Consistency
A ping cannot also carry text.
Availability
Adding a variant fails the build until every match is updated.
Failure assumptions
- The message is a struct with every field optional.
- A string tag is matched with a default arm.
Constraints
- Stay on types. Do not introduce a trait object yet.
Prompt
A socket message is a ping, a text payload, or a coordinate pair. Callers must handle a new variant.
Sum type versus flags
Prefer
Enum with data
One variant is live. Match names every shape.
- Illegal pairs cannot be built.
- New variants fail the build.
- Option is Some or None.
Alternative
Flags and null
Bool pairs and null leave states the domain does not want.
- Every caller re-validates.
- A default arm hides a new case.
- Absence is a sentinel.
Overview
A struct is the product: every field together, and the struct owns them. An enum is the sum: one variant, which may carry its own data. match must cover the variants. Option is that enum for absence.
Comparative
| Idea | TypeScript | Python | Rust |
|---|---|---|---|
| Record | interface or class | dataclass | struct |
| Sum type | union | Union or Enum | enum with data |
| Null | T or null | Optional | Option |
| Exhaustiveness | never tricks | runtime match | match |
Decisions
- 1
Step 1 Model data - struct for all-of, enum for one-of
- nextStep 2 Put data inside variants - Text(String), Move with x and y
- 2
Step 2 Put data inside variants - Text(String), Move with x and y
- nextStep 3 match on the value
- 3
Step 3 match on the value
- nextStep 4 Every variant handled
- ?
Step 4 Every variant handled
- yesStep 5 Compiles - each arm binds its fields
- noFailure path - E0004 non-exhaustive patterns
- 5
Step 5 Compiles - each arm binds its fields
- nextStep 6 Option and Result are enums too - if let and the question-mark operator
- 6
Failure path - E0004 non-exhaustive patterns
- 7
Step 6 Option and Result are enums too - if let and the question-mark operator
Lesson map
Types, Structs, Enums & Pattern Matching
Structs, enums, pattern matching, Option as enum.
Architecture. Step 1 Model data - struct for all-of, enum for one-of Ready. Step 2 Put data inside variants - Text(String), Move with x and y Ready. Step 3 match on the value Ready. Step 4 Every variant handled Ready. Step 5 Compiles - each arm binds its fields Ready. Failure path - E0004 non-exhaustive patterns Ready. Step 6 Option and Result are enums too - if let and the question-mark operator Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB A["Step 1 Model data - struct for all-of, enum for one-of Ready"] B["Step 2 Put data inside variants - Text(String), Move with x and y Ready"] C["Step 3 match on the value Ready"] D["Step 4 Every variant handled Ready"] E["Step 5 Compiles - each arm binds its fields Ready"] F["Failure path - E0004 non-exhaustive patterns Ready"] G["Step 6 Option and Result are enums too - if let and the question-mark operator Ready"] A -->|continues| B B -->|continues| C C -->|continues| D D -->|yes| E D -->|no| F E -->|continues| G
Press Run. Snippets must be self-contained — no network, files, or native modules.
Rosetta — struct, class, dataclass
struct User {
id: u64,
email: String,
}
impl User {
fn new(id: u64, email: impl Into<String>) -> Self {
Self { id, email: email.into() }
}
}class User {
constructor(public id: number, public email: string) {}
}
const u = new User(1, "a@b.co");
console.log(u.email);from dataclasses import dataclass
@dataclass
class User:
id: int
email: str
u = User(1, "a@b.co")
print(u.email)Methods live in impl. There is no base class. email: String owns the buffer.
Rosetta — enum versus discriminated union
enum Msg {
Ping,
Text(String),
Move { x: i32, y: i32 },
}
fn handle(m: Msg) -> String {
match m {
Msg::Ping => "pong".into(),
Msg::Text(s) => format!("echo:{s}"),
Msg::Move { x, y } => format!("{x},{y}"),
}
}type Msg =
| { kind: "ping" }
| { kind: "text"; value: string }
| { kind: "move"; x: number; y: number };
function handle(m: Msg): string {
switch (m.kind) {
case "ping":
return "pong";
case "text":
return `echo:${m.value}`;
case "move":
return `${m.x},${m.y}`;
}
}from dataclasses import dataclass
@dataclass
class Ping:
pass
@dataclass
class Text:
value: str
@dataclass
class Move:
x: int
y: int
def handle(m: Ping | Text | Move) -> str:
match m:
case Ping():
return "pong"
case Text(value=s):
return f"echo:{s}"
case Move(x=x, y=y):
return f"{x},{y}"
case _:
raise TypeError(type(m))Rust match is exhaustive on the enum. TypeScript needs a discriminant and still benefits from a never check. Python 3.10 match helps, and a wildcard can hide a missed case.
Rosetta — Option versus null versus None
fn domain(email: &str) -> Option<&str> {
email.split_once('@').map(|(_, d)| d)
}function domain(email: string): string | undefined {
const i = email.indexOf("@");
return i >= 0 ? email.slice(i + 1) : undefined;
}def domain(email: str) -> str | None:
if "@" in email:
return email.split("@", 1)[1]
return Nonemap and if let Some replace a chain of null tests. Libraries should not unwrap the Option away.
Interview Q&A
Why an enum instead of bool flags?
Answer
Illegal combinations become unrepresentable. Two bools allow states the domain does not have.
if let or match?
Answer
if let focuses on one pattern. match is the multi-way form the compiler checks for exhaustiveness.
Is Option a null pointer?
Answer
No. It is an enum with Some and None. Handle it or propagate it.
Do Rust structs inherit?
Answer
Not by default. Shared behavior is a trait, covered next. The struct owns its fields.
How is a TypeScript union different?
Answer
You usually add a kind field yourself. A missed case is easier to ship unless the switch ends in never.
How is Python match different?
Answer
It is a runtime test. A wildcard arm can swallow a new class. Rust fails the build.
What does a struct field of type String mean?
Answer
The struct owns that buffer. Dropping the struct drops the string, unless the field was moved out.
Which compiler checks these matches?
Answer
rustc 1.98.1, with crates on edition 2024. Exhaustiveness is a compiler rule, not a lint you turn on later.
Pitfalls
- Optional fields that encode a variant.
- A wildcard that should have named the new arm.
- unwrap on None in a library.
- String status codes instead of variants.
- Copying a whole String out of a match when a reference would do.
Take is_ping and has_text on one struct. Rewrite the legal states as three variants and name the combination that disappears.