Generic Structs
How to define and use structs with generic type parameters in Rust to write flexible and reusable code without repeating definitions
A struct in Rust groups related pieces of data under one name. Without generics, you would write a separate struct for every combination of field types you need. A Point that holds i32 coordinates, another Point for f64, another for u32—each identical in shape, differing only in the types inside. That duplication makes code harder to maintain and obscures the underlying concept: a point is a point, regardless of the number type used for its coordinates.
Generic structs solve this by letting you define the shape once and plug in different concrete types later, at compile time.
Defining a Generic Struct
The syntax is a straightforward extension of how you declare a regular struct. After the struct name, you place a comma‑separated list of type parameter names inside angle brackets. Those parameters then replace the concrete types on one or more fields.
struct Point<T> {
x: T,
y: T,
}
Point<T> is read as “Point is generic over some type T.” Both x and y must be the same type because they share the same type parameter. This constraint is deliberate: many geometric operations only make sense when both coordinates live in the same numeric space.
Creating an instance looks exactly like a non‑generic struct. Rust infers the concrete type from the values you provide.
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
The compiler sees that 5 and 10 are integers, so it stamps out a Point<i32>. For 1.0 and 4.0, it stamps out a Point<f64>. Both exist in the same program, generated from the same source definition.
Mixing types with a single generic parameter:
If you try to create a Point where x is an integer and y is a float, the compiler rejects it.
let wont_work = Point { x: 5, y: 4.0 };
// error[E0308]: mismatched types
The moment x is set to 5, the compiler locks T to the integer type. When y is then set to 4.0, it sees a float where an integer was expected. The fix is to introduce a second type parameter if you genuinely need fields of differing types.
Multiple Type Parameters
When the fields should be independent, add more type parameters.
struct Point<T, U> {
x: T,
y: U,
}
Now x and y can be completely different types, and all of the following are valid:
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
let string_and_bool = Point { x: "origin", y: true };
Using more than two or three type parameters is rare in practice and often a signal that the struct is trying to do too much. When you see four or five type parameters, consider whether the structure can be broken into smaller, purpose‑specific pieces.
Too many type parameters hurts readability:
A struct with many generic parameters, like struct Configuration<A, B, C, D, E>, becomes difficult to use. Readers cannot easily remember what each parameter represents, and the type signatures in error messages grow long. If you find yourself in this situation, grouping related fields into their own generic structs is usually cleaner.
How the Compiler Sees Generic Structs
A useful mental model: think of a generic struct definition as a template. When you write Point<i32>, the compiler copies the template and replaces every T with i32, producing a unique concrete struct. This process—called monomorphization—happens at compile time for every combination of type arguments you actually use in your code.
The key insight for beginners: Rust never stores a “generic” struct at runtime. There is no Point<T> floating around in memory waiting to be filled. The final binary contains only the fully‑typed versions: Point<i32>, Point<f64>, and so on. The performance and memory layout are identical to what you would get if you had handwritten each version by hand.
Monomorphization is covered in detail in the next section; for generic structs, the takeaway is that you get zero‑cost abstraction—the flexibility of generics with no runtime overhead.
Adding Methods to Generic Structs
Methods on a generic struct are written inside an impl block that also declares the type parameters. You must repeat the parameter names after impl so Rust knows you are implementing methods for all possible concrete versions of the struct, not just one.
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
fn y(&self) -> &T {
&self.y
}
}
The impl<T> line says: “for any type T, we are implementing methods on Point<T>.” Inside the block, self has type &Point<T>, and self.x has type T. The methods x and y simply return references to the fields; they work whether T is i32, f64, String, or any other type.
If a method needs the generic type to support a specific operation—such as numeric addition or comparison—you add the required trait bound on that particular impl block.
use std::ops::Add;
impl<T: Add<Output = T>> Point<T> {
fn add_points(self, other: Point<T>) -> Point<T> {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
Here T: Add<Output = T> tells the compiler: only implement add_points for Point<T> when T itself supports addition and returns the same type. For Point<i32> and Point<f64> this works; for Point<String> it does not. Restricting the method to types that can be added means the compiler catches misuse at the call site, not deep inside add_points.
Trait bounds belong on impl blocks, not struct definitions:
It is idiomatic Rust to keep the struct definition itself free of trait bounds unless the struct’s fields literally cannot exist without a certain capability (an extremely rare situation). Bounds on impl blocks are preferred because they let you implement different sets of methods for different type capabilities. For example, you might implement Display for Point<T> only when T: Display, without requiring Display for all Points. This granularity keeps the API flexible.
Common Mistakes and Misconceptions
Several patterns trip up developers new to generic structs. Being aware of them early saves time with compiler errors.
Forgetting impl<T> when implementing methods. Omitting the type parameter on the impl block makes Rust think T is a concrete type name, like i32. You’ll get an error saying T cannot be found, because the compiler is looking for a type named T in the current scope. Always write impl<T> Point<T> unless you are implementing methods for a specific instantiation, like impl Point<f32>.
Expecting automatic conversion between generic instantiations. A Point<i32> and a Point<f64> are completely unrelated types. You cannot pass one to a function expecting the other, and they do not share method implementations unless those implementations are generic over all T. If you need to convert, you must write an explicit conversion method (like fn into_f64(self) -> Point<f64>).
Assuming the struct itself imposes trait bounds. Even if you plan to use Point only with numbers, the struct definition remains unbounded. Until you write an impl block with a bound, the compiler allows you to create Point<String>, Point<bool>, or Point<File>—even if those don’t make sense. The bounds appear where the operations are needed, not on the data container.
Misunderstanding Copy and Clone with generics. A generic struct is Copy only if every field is Copy. When you derive Copy on a generic struct, Rust adds an implicit T: Copy bound automatically. The same applies to Clone, Debug, and most other derivable traits. If you later try to use Point<String> when String is not Copy, you will get a compile error. This is correct behavior, not a bug—the compiler is protecting you from accidental expensive copies.
What Rust Does Not Support
Developers coming from C++ sometimes look for variadic type parameters—the ability to write a struct that accepts an arbitrary number of type arguments, like a template parameter pack. Rust does not have this feature. You cannot write something like struct Tuple<Ts...> to hold a variable‑length list of types.
The idiomatic replacement depends on the use case. If you need a fixed but configurable number of types, use a regular generic struct with the exact number of parameters you need. If the number truly varies, macro‑based approaches like the tuple type or the hlist crate are available, but these are advanced patterns. For most applications, the lack of variadic generics is not a limitation—it encourages designing around concrete, named fields rather than arbitrary type lists.
Reusing generic type parameter names across structs:
It is common to see T used in many different struct definitions. Each T is local to its own definition; a Point<T>’s T has nothing to do with a Rectangle<T>’s T in the same module. This scoping means you can name type parameters intuitively without worrying about global collisions.
Real‑World Usage of Generic Structs
Generic structs appear throughout the Rust ecosystem in precisely the situations where a shape is independent of the payload type.
- Containers and wrappers. The standard library’s
Vec<T>,HashMap<K, V>, andBox<T>are all generic structs. Their job—storing, retrieving, and owning data—does not depend on the stored type. - Domain‑modeling pairs and results. The
Result<T, E>type is a generic struct (represented as an enum under the hood, but the idea is the same). It separates the shape of “success or failure” from the specific types of the success value and error. - Configuration and context bundles. A web framework might define
struct AppState<DB>where the database backend is generic, allowing the same handler code to work with an in‑memory test database or a production PostgreSQL connection. - Scientific and mathematical code. A
Vector<T, const N: usize>can represent a fixed‑size mathematical vector over any numeric type, letting the same algorithms handlef32andf64without duplication.
In every case, the struct definition captures the relationship between the fields, while the type parameters defer the choice of concrete types to the user of the struct.
Summary
Generic structs let you define a shape once and reuse it across many types. The syntax is minimal: <T> after the struct name, then T wherever a concrete type would go. When fields need independent types, introduce additional parameters like <T, U>. Methods are written in impl<T> blocks; trait bounds are added only where the method’s body requires specific behavior.
The mental model of a compile‑time template that stamps out concrete versions keeps the abstraction intuitive. Monomorphization ensures that using a generic struct costs nothing at runtime compared to a hand‑written, type‑specific version.