Generic Enums

How to define enums that can hold values of any type using generic type parameters in Rust

Rust’s enums do more than list constants — each variant can carry data. Without generics, you would need a separate enum definition for every possible payload type. A generic enum solves this by letting you write the structure once and plug in any concrete type later. The two most common examples in Rust are Option<T> (an optional value of any type) and Result<T, E> (a success value or an error, each of its own type).

Defining a Generic Enum

A generic enum declares one or more type parameters in angle brackets right after the enum name, then uses those parameters inside the variant definitions. This is exactly the same syntax used for generic structs and functions.

// Declares an enum generic over type T
enum Slot<T> {
    Occupied(T),
    Empty,
}

The type parameter T is a placeholder. When you create a Slot<i32>, the compiler knows Occupied holds an i32. When you create a Slot<String>, it holds a String. The Empty variant does not store any value, so it remains independent of T.

Type parameter naming:

By convention, generic type parameters use short CamelCase names, usually a single letter like T, U, or E. The name T is the default when there is only one type to abstract over.

Multiple Type Parameters

An enum is not limited to a single generic type. Multiple type parameters are separated by commas, just like in structs.

enum Either<L, R> {
    Left(L),
    Right(R),
}

Here Either<L, R> can hold a value of type L in the Left variant, or a value of type R in the Right variant. Each type is independent — Either<i32, String> is a completely different type from Either<String, i32>.

The real power of multiple type parameters becomes clear with the standard library’s Result:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Ok carries the success value of some type T, and Err carries the error value of some type E. When you open a file, T might be std::fs::File and E might be std::io::Error. The same Result enum handles networking, parsing, and any other fallible operation — each with its own pair of types.

Option\u003cT\u003e and Result\u003cT, E\u003e in Practice

You rarely need to define your own generic enums because Option and Result are already deeply integrated into the language and standard library. Every time you call a method that may or may not return a value, you get an Option<T>. Every time you call something that can fail, you get a Result<T, E>.

fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}
fn main() {
    let result = divide(10.0, 2.0);
    match result {
        Some(value) => println!("Result: {}", value),
        None => println!("Cannot divide by zero"),
    }
}

When result is Some(value), pattern matching extracts the f64 inside. The Option<f64> type tells the compiler: this variable is either a floating-point number or nothing at all. You cannot accidentally use the value without handling the None case — the compiler enforces it.

The compiler checks both cases:

When you pattern match on an Option<T> or Result<T, E>, Rust ensures every possible variant is handled. Forgetting to handle None or Err is a compile error, not a runtime bug.

Why Enum Variants Are Not Types

A frequent point of confusion for newcomers is trying to use an enum variant as a type. For example:

enum Role {
    User,
    Admin,
}
struct Session<T> {
    id: i64,
    role: T,
}
// This will not compile:
fn only_for_user(s: Session<Role::User>) { /* ... */ }

The compiler rejects Session<Role::User> because Role::User is a variant — a value constructor — not a type. You cannot parameterize a generic struct or enum with a variant. The type is Role, and its values are Role::User and Role::Admin.

Variants are values, not types:

Writing Session<Role::User> attempts to use a variant where a type is expected. Rust will point you to the enum itself as the type you probably meant, but Session<Role> would then accept both User and Admin — not the restriction you intended.

If you truly need a type-level distinction between variants, the idiomatic approach is to create separate types and wrap them inside the enum.

struct User;
struct Admin;
enum Role {
    User(User),
    Admin(Admin),
}
struct Session<T> {
    id: i64,
    role: T,
}
fn only_for_user(s: Session<User>) {
    println!("Session for user: {}", s.id);
}

Now User and Admin are real types. Session<User> is a Session whose role field holds a User struct, and Session<Admin> is a distinct type. The Role enum can still be useful if you need to store both possibilities in a single collection or return them from a function, but the type-level restriction is achieved through the inner structs.

Common Misconceptions When Working With Generic Enums

“I can use T like a concrete type inside the enum.”
Defining a generic enum only declares that the enum can hold values of any type. It does not grant any capabilities on those values. You cannot compare two Ts, add them, print them, or call methods on them unless you constrain T with a trait. Attempting to do so will produce a compile error that suggests adding a trait bound.

No operations allowed without trait bounds:

Code like if a == b inside a function that receives Option<T> will fail unless T implements PartialEq. Rust will tell you exactly which trait is missing. This is not a runtime failure — the compiler catches it and refuses to build.

“Generic enums create new types at runtime.”
Rust uses monomorphization: the compiler generates a separate concrete version of the enum for each combination of type arguments used in your code. Option<i32> and Option<String> become two distinct types with their own layout and methods, all decided at compile time. There is no runtime type erasure, no boxing, and no performance overhead.

“I can match on the concrete type inside a generic function.”
You cannot write match val { Some(i32) => ... } because T is not a concrete type inside the generic definition. The matching is on the shape of the enum (the variants), not on the type argument. To branch on the type, you need trait objects or enum dispatch, which are separate concepts covered later.

Where You’ll Encounter Generic Enums Beyond the Standard Library

Generic enums are not limited to Option and Result. They appear whenever a piece of data can be one of several shapes, and the shapes share a common abstract role but differ in their contents.

  • Abstract syntax trees (ASTs) — an expression node might be a literal i32, a binary operation with two sub‑expressions, or a variable reference. Using a generic Expr<T> lets the same AST structure represent arithmetic on integers or strings.
  • Message passing — in event‑driven systems, you might define enum Event<T> { Data(T), Shutdown } to send payloads of arbitrary type through a channel.
  • Custom error handling — libraries often define their own Error<T> enum variants that wrap specific domain errors alongside a generic message.

In every case, the pattern is the same: define the shape once with type parameters, and let the compiler stamp out the concrete versions for you.

Summary

Generic enums give you a way to write one definition that describes all possible variants regardless of the data they carry. Option<T> and Result<T, E> are the most visible examples, but the pattern extends to any situation where variants differ in the type of value they hold.

The key insight is that a generic enum captures a relationship between variants and types, not a specific type itself. Variants are not types, and generic parameters carry no abilities until you constrain them with traits. Once you understand that generic enums are a compile‑time tool for eliminating duplicate definitions, you can make those type parameters useful through trait bounds.