What Are Generic Types in Rust?

Generic types in Rust are placeholders that let you write flexible, reusable code for any data type without duplication.

The copy-paste trap

You write a function to find the maximum value in a slice. It works perfectly for i32. Then you need the same logic for f64. You copy the function, paste it, rename it to max_f64, and change the type signature. A week later, you need it for String. You copy-paste again.

Now you have three nearly identical functions. If you find a bug in the comparison logic, you have to fix it in three places. If you add a new type, you copy-paste again. This is the copy-paste trap. It leads to bloated code and inevitable bugs where one version gets updated and the others don't.

Generics are the escape hatch. They let you write the logic once and use it with any type. You define a placeholder for the type, and the compiler fills in the blanks for you. You get reusability without sacrificing type safety or performance.

Generics are type variables

A generic type is a placeholder in your code that gets replaced with a concrete type when you use it. Think of it like a variable, but for types. Just as let x = 5 binds the value 5 to the name x, a generic parameter binds a type to a name like T.

You declare generic parameters using angle brackets. In a function, they go after the function name. In a struct or enum, they go after the name. The compiler treats T as an unknown type until you call the function or instantiate the struct. At that point, the compiler checks that the concrete type satisfies all the requirements your code imposes, and it generates specialized code for that type.

This process is called monomorphization. The compiler takes your generic code and produces a separate, concrete version for every type you actually use. If you use a generic function with i32 and String, the compiler generates two functions in the final binary: one for i32 and one for String. You write the code once, but the binary contains optimized code for each type.

Generics give you the flexibility of dynamic typing with the guarantees of static typing. The compiler knows the exact type at every point in your code. You get full type checking, no runtime overhead, and the ability to reuse logic across types.

Minimal example

Here is a generic struct and a generic function. The struct Point holds two coordinates of the same type. The function largest finds the maximum value in a slice.

/// A point in 2D space where coordinates can be any type.
/// The type T is determined when you create a Point.
struct Point<T> {
    x: T,
    y: T,
}

/// Finds the largest item in a slice.
/// The compiler requires T to implement PartialOrd so we can compare items.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    // The compiler infers T as i32 from the slice type.
    let numbers = vec![10, 5, 20, 3];
    println!("Largest number: {}", largest(&numbers));

    // The compiler infers T as f64 here.
    let floats = vec![1.1, 2.2, 0.5];
    println!("Largest float: {}", largest(&floats));

    // Point can hold any type, but both coordinates must match.
    let integer_point = Point { x: 1, y: 2 };
    let float_point = Point { x: 1.0, y: 2.0 };
}

The function signature fn largest<T: PartialOrd> tells the compiler two things. First, T is a generic type parameter. Second, T must implement the PartialOrd trait. The PartialOrd trait provides comparison operators like > and <. Without this bound, the compiler wouldn't know how to compare items of type T, and the code would not compile.

Rustaceans follow a naming convention for generic parameters. Use T for the first type, U for the second. For map-like structures, use K for key and V for value. These names carry no semantic meaning to the compiler, but they help humans read the code.

Generics are zero-cost abstractions. The flexibility comes for free at runtime.

Monomorphization: how the compiler makes it fast

When you call largest(&[10, 5, 20]), the compiler looks at the argument. It sees a slice of i32. It infers that T must be i32. It then checks the trait bound. i32 implements PartialOrd, so the check passes.

Next, the compiler generates a concrete function. It replaces every occurrence of T with i32. The result is a function that looks like this:

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

This function is no longer generic. It works only with i32. The compiler can optimize it aggressively. It knows the size of i32, the alignment requirements, and the exact machine instructions for comparison. If you call largest with f64, the compiler generates a separate largest_f64 function.

At runtime, there are no generics. There is no type checking. There is no dispatch table. The binary contains direct calls to specialized functions. This is why generics are fast. The cost is paid at compile time, not runtime.

Monomorphization can increase binary size if you use a large generic structure with many different types. Each combination produces a copy of the code. In most applications, this is not a problem. The compiler's Link Time Optimization (LTO) can often deduplicate identical code. If binary size becomes critical, you can mitigate bloat by using trait objects or limiting the number of type combinations.

Generics give you specialization without sacrifice. The compiler does the heavy lifting.

Realistic example: structs and methods

Generic structs often need methods. Methods can have their own generic parameters and trait bounds, independent of the struct's parameters. This allows you to define operations that only make sense for certain types.

/// A wrapper that adds a label to any value.
struct Labeled<T> {
    value: T,
    label: String,
}

impl<T> Labeled<T> {
    /// Creates a new Labeled instance.
    fn new(value: T, label: &str) -> Self {
        Labeled {
            value,
            label: label.to_string(),
        }
    }

    /// Returns a reference to the inner value.
    fn get_value(&self) -> &T {
        &self.value
    }
}

// This impl block only applies when T implements std::fmt::Display.
// Methods here are only available for types that can be printed.
impl<T: std::fmt::Display> Labeled<T> {
    /// Formats the labeled value as a string.
    fn display(&self) -> String {
        format!("{}: {}", self.label, self.value)
    }
}

fn main() {
    let labeled_int = Labeled::new(42, "Answer");
    // get_value is available for all T.
    println!("Value: {}", labeled_int.get_value());
    // display is available because i32 implements Display.
    println!("{}", labeled_int.display());
}

The first impl<T> block defines methods that work for any type T. The second impl<T: std::fmt::Display> block adds methods that require T to implement Display. If you try to call display on a Labeled containing a type that doesn't implement Display, the compiler rejects the call.

When a function signature becomes cluttered with generic parameters and bounds, you can use a where clause to improve readability. The where clause moves the bounds to a separate line.

/// A function with multiple generic parameters and bounds.
/// The where clause keeps the signature clean.
fn combine<T, U>(first: T, second: U) -> String
where
    T: std::fmt::Display,
    U: std::fmt::Display,
{
    format!("{} {}", first, second)
}

The where clause is a style choice. Use it when the signature is hard to read. The compiler treats where clauses and inline bounds identically.

Methods can also introduce new generic parameters. A method on Vec<T> might take a parameter of type U. This is common in iterators and adapters. The method's generics are independent of the struct's generics.

Trait bounds are the contract. They tell the compiler what capabilities the type must have.

Pitfalls and compiler errors

Generics are powerful, but they come with constraints. The compiler enforces these constraints strictly. Here are the common pitfalls.

Missing trait bounds. If you use an operator or method on a generic type, the compiler requires the corresponding trait bound. If you write item > largest without T: PartialOrd, the compiler rejects you with E0277 (trait bound not satisfied). The error message tells you exactly which trait is missing and suggests adding the bound.

Over-constraining. Adding bounds you don't need limits the usability of your generic code. If a function only needs T: Clone, don't add T: Debug + Eq + Hash just because you might need them later. Each bound restricts the set of types that can be used. Keep bounds minimal. Add them only when the logic demands them.

Monomorphization bloat. If you use a generic function with dozens of different types, the compiler generates dozens of copies. This can increase compile time and binary size. If you hit this limit, consider whether you really need generics. Sometimes a concrete type or a trait object is more appropriate. Profile before optimizing.

Type inference failures. The compiler infers generic types from usage. If there isn't enough information, inference fails. You'll see an error like "cannot infer type for T". In these cases, provide an explicit type annotation. You can specify the type at the call site using the turbofish syntax: function::<i32>(args).

Mixing types in generic structs. A generic struct like Point<T> requires all fields of type T to be the same type. You cannot create Point { x: 1, y: "a" }. If you need different types, use multiple generic parameters: Point<T, U>.

The compiler errors are your friend. They point directly to the missing piece. Trust the bounds. They are the safety net that makes generic code correct.

When to use generics

Generics are the default tool for reusable code in Rust. Use them whenever you need type safety and performance. The decision matrix below helps you choose the right approach.

Use generics when you need type safety and zero-cost abstraction. The compiler checks everything at compile time, and there is no runtime overhead.

Use generics when the same logic applies to multiple types, like a container, a wrapper, or a comparison function.

Use generics when you want to enforce constraints via trait bounds. Bounds ensure the type has the capabilities your code requires.

Reach for concrete types when performance profiling shows the generic expansion is causing binary bloat, or when you only ever use one type. Concrete types avoid the overhead of generating multiple copies.

Reach for trait objects (dyn Trait) when you need a heterogeneous collection of different types behind a single interface, and you can accept the runtime dispatch cost. Trait objects allow you to store Box<dyn Shape> containing a Circle and a Square. Generics cannot do this; a Vec<Shape<T>> must hold one concrete type.

Generics are the foundation of reusable Rust code. Start with generics. Switch to trait objects only when you need dynamic heterogeneity.

Where to go next