Question

Can a Struct Extend Another Struct in Rust? Composition vs Inheritance

rustinheritancestruct

Question

I am using Rust and learning it from a JavaScript background. I want to know whether one struct can extend another struct so that the new struct automatically contains all the fields of the original struct.

For example, in JavaScript with ES6 classes, I could write:

class Person {
    constructor(gender, age) {
        this.gender = gender;
        this.age = age;
    }
}

class Child extends Person {
    constructor(name, gender, age) {
        super(gender, age);
        this.name = name;
    }
}

I would like to do something conceptually similar in Rust.

A constraint is that the original struct comes from an external Cargo package, so I cannot modify its definition.

I found older material about single inheritance and virtual structs, but that feature was removed. I also found suggestions about using traits, but since the original type is from another crate, I do not think I can convert it into a trait.

What is the correct Rust approach for this situation?

Short Answer

By the end of this page, you will understand why Rust structs do not support field inheritance like JavaScript or classical OOP languages, and what to use instead. You will learn the idiomatic Rust solution: composition—putting one struct inside another—plus how traits and wrapper types help you model shared behavior safely and clearly.

Concept

Rust does not support struct inheritance where one struct automatically gains all the fields of another struct.

If you have a type from another crate and want your own type to include it, the standard Rust solution is composition:

struct Child {
    person: Person,
    name: String,
}

This means Child contains a Person instead of being a specialized Person through inheritance.

Why Rust avoids field inheritance

Rust is designed around:

  • clear ownership
  • explicit data layout
  • predictable behavior
  • composition over inheritance

Inheritance can blur the structure of data and create tight coupling. Rust prefers simpler building blocks:

  • structs for storing data
  • impl blocks for methods
  • traits for shared behavior
  • composition for combining types

Important point about external types

Even if Person comes from another crate, you can still use it as a field inside your own struct. You do not need to modify Person to do that.

What you lose compared to inheritance

With inheritance, you might write child.age directly if age came from Person.

With composition, you usually write:

child.person.age

That extra level is intentional: Rust makes structure explicit.

What you can still do

You can make your wrapper more convenient by adding methods:

impl Child {
    fn age(&self) -> u32 {
        self.person.age
    }
}

If you need shared behavior rather than shared fields, traits are often the right abstraction.

Mental Model

Think of Rust structs like real-world objects packed in boxes.

  • In inheritance-based languages, a Child box might be treated as a larger Person box with extra compartments added on.
  • In Rust, a Child box usually contains a separate Person box inside it.

So instead of saying:

  • “Child is a Person with more fields”

Rust often says:

  • “Child has a Person and also has more fields”

This is called composition.

A simple analogy:

  • Inheritance: building a taller version of the same cabinet
  • Composition: placing one cabinet inside a larger storage unit with extra drawers

Rust strongly prefers the second approach because it is explicit and easier to reason about.

Take Quiz

Syntax and Examples

The core Rust pattern is to store the existing struct as a field.

struct Person {
    gender: String,
    age: u32,
}

struct Child {
    person: Person,
    name: String,
}

You create values like this:

fn main() {
    let person = Person {
        gender: String::from("female"),
        age: 8,
    };

    let child = Child {
        person,
        name: String::from("Mia"),
    };

    println!("{} is {} years old", child.name, child.person.age);
}

If the original struct comes from another crate

That is still fine. You can do this:

use external_crate::Person;

struct Child {
    person: Person,
    name: String,
}

Adding convenience methods

If you want Child to expose part of Person, add methods:

struct Person {
    gender: String,
    age: u32,
}

struct Child {
    person: Person,
    name: String,
}

impl Child {
    fn age(&self) -> u32 {
        self.person.age
    }

    fn gender(&self) -> &str {
        &self.person.gender
    }
}

Now usage is simpler:

fn main() {
    let child = Child {
        person: Person {
            gender: String::from("male"),
            age: 10,
        },
        name: String::from("Leo"),
    };

    println!("{} is a {} year old {}", child.name, child.age(), child.gender());
}

Using a trait for shared behavior

If multiple types should behave like a person, use a trait:

trait HasAge {
    fn age(&self) -> u32;
}

impl HasAge for Person {
    fn age(&self) -> u32 {
        self.age
    }
}

impl HasAge for Child {
    fn age(&self) -> u32 {
        self.person.age
    }
}

This does not copy fields, but it lets different types share an interface.

Step by Step Execution

Consider this example:

struct Person {
    gender: String,
    age: u32,
}

struct Child {
    person: Person,
    name: String,
}

fn main() {
    let p = Person {
        gender: String::from("female"),
        age: 7,
    };

    let c = Child {
        person: p,
        name: String::from("Ava"),
    };

    println!("{} is {} years old", c.name, c.person.age);
}

Step by step

  1. Person is defined with two fields:

    • gender
    • age
  2. Child is defined with:

    • person, which stores a full Person
    • name, which is specific to Child
  3. In main, a Person value is created and stored in p.

  4. Then a Child value is created.

    • The field person: p moves the Person into Child.
    • The field name stores a separate string.
  5. When printing, c.name accesses the child's own field.

  6. c.person.age accesses the nested Person field inside Child.

Key observation

Rust does not flatten the fields of Person into Child.

So this is not valid:

// not valid
// println!("{}", c.age);

You must use the path through the contained struct:

println!("{}", c.person.age);

Real World Use Cases

Composition is used constantly in real Rust programs.

API request context

You may wrap a library type with your app-specific metadata:

struct AuthenticatedRequest {
    request: HttpRequest,
    user_id: String,
}

Database models

You might store a shared record and add view-specific fields:

struct UserProfile {
    user: User,
    is_online: bool,
}

Configuration objects

A crate may provide a base config type, and your app adds environment-specific settings:

struct AppConfig {
    base: ExternalConfig,
    debug_mode: bool,
}

Parsing and validation

A parsed object from a library can be wrapped with validation results:

struct CheckedDocument {
    document: ExternalDocument,
    is_valid: bool,
}

UI or game state

A reusable type can be embedded inside a richer application struct:

struct PlayerState {
    person: Person,
    score: u32,
}

In all these cases, composition keeps external types usable while letting your code add its own data.

Take Quiz

Real Codebase Usage

In real Rust projects, developers usually combine composition with a few common patterns.

1. Wrapper structs

A type from another crate is placed inside your own type.

struct Child {
    person: Person,
    name: String,
}

This is the most common solution.

2. Convenience methods

To avoid repeated nested access, developers add methods:

impl Child {
    fn age(&self) -> u32 {
        self.person.age
    }
}

This creates a cleaner public API.

3. Validation at construction time

A constructor can check inputs before creating the struct:

impl Child {
    fn new(person: Person, name: String) -> Self {
        Self { person, name }
    }
}

If validation is needed:

impl Child {
    fn try_new(person: Person, name: String) -> Result<Self, String> {
        if name.trim().is_empty() {
            return Err(String::from("name cannot be empty"));
        }

        Ok(Self { person, name })
    }
}

4. Traits for shared behavior

If several types should expose the same operations, use a trait instead of inheritance.

trait Named {
    fn name(&self) -> &str;
}

5. Delegation-style methods

If a contained type has useful methods, your outer type can forward to them.

impl Child {
    fn gender(&self) -> &str {
        &self.person.gender
    }
}

This is sometimes called manual delegation.

6. Newtype pattern

If you only want to wrap one existing type, a tuple struct may be enough:

struct WrappedPerson(Person);

This is useful when adding methods, validation, or trait implementations without changing the original type.

Common Mistakes

Mistake 1: Expecting field inheritance

Beginners often try to access inner fields directly as if they were inherited.

struct Child {
    person: Person,
    name: String,
}

// wrong idea
// let age = child.age;

Use:

let age = child.person.age;

Or add a method:

impl Child {
    fn age(&self) -> u32 {
        self.person.age
    }
}

Mistake 2: Thinking traits copy fields

Traits define behavior, not stored data.

trait HasAge {
    fn age(&self) -> u32;
}

A trait cannot automatically add an age field to a struct.

Mistake 3: Assuming you must modify the external crate

You do not need to change the original struct to compose with it.

use external_crate::Person;

struct Child {
    person: Person,
    name: String,
}

This is valid and idiomatic.

Mistake 4: Confusing composition with duplication

You usually should not copy every field manually into a new struct unless there is a good reason.

Less ideal:

struct Child {
    gender: String,
    age: u32,
    name: String,
}

This duplicates the structure and can drift from the original type.

Mistake 5: Forgetting ownership rules

When you move a Person into Child, you generally cannot use the original binding afterward unless the type implements Copy.

let p = Person {
    gender: String::from("male"),
    age: 9,
};

let c = Child {
    person: p,
    name: String::from("Noah"),
};

// p is moved here and usually cannot be used again

Mistake 6: Overusing inheritance thinking from other languages

In Rust, ask these questions instead:

  • Do I need to store another type? Use composition.
  • Do I need to share behavior? Use traits.
  • Do I need to wrap a type with extra meaning? Use a wrapper or newtype.

Comparisons

ConceptWhat it doesIncludes fields automatically?Best use
InheritanceOne type extends anotherYes in OOP languagesClassical OOP hierarchies
CompositionOne struct contains anotherNoIdiomatic Rust data modeling
TraitShares behavior through methodsNoCommon interfaces across types
NewtypeWraps a single existing typeNoAdd meaning, methods, or trait impls

Composition vs inheritance

QuestionInheritance-style answerRust-style answer
How do I reuse fields?Extend the base classStore the struct as a field
How do I share methods?Inherit methodsImplement methods or traits
How do I model “has a”?Often awkwardNatural fit
How do I model “acts like”?Base class interfaceTrait

Composition vs manual duplication

ApproachProsCons
CompositionReuses the original type safelyAccess is nested
Duplicate fields manuallyFlat accessRepetition, drift, maintenance cost

Trait vs struct field

NeedUse trait?Use contained struct?
Shared behavior onlyYesMaybe not needed
Shared stored dataNoYes
External type reuseSometimesYes
Add app-specific dataNoYes
Take Quiz

Cheat Sheet

// Composition: preferred Rust approach
struct Child {
    person: Person,
    name: String,
}

Rules to remember

  • Rust structs do not support field inheritance.
  • Use composition to include one struct inside another.
  • Use traits to share behavior, not fields.
  • External crate types can still be used as fields in your own structs.
  • Nested access looks like child.person.age.
  • Add methods if you want a simpler API.

Common patterns

impl Child {
    fn new(person: Person, name: String) -> Self {
        Self { person, name }
    }

    fn age(&self) -> u32 {
        self.person.age
    }
}

If you only need behavior

trait HasAge {
    fn age(&self) -> u32;
}

If you only need a wrapper

struct WrappedPerson(Person);

Quick decision guide

  • Need another type's data inside yours? -> composition
  • Need a shared method interface? -> trait
  • Need to attach meaning to one existing type? -> newtype
  • Need inherited fields like OOP classes? -> not available in Rust

FAQ

Can one struct inherit fields from another in Rust?

No. Rust does not support struct field inheritance. Use composition instead.

What is the Rust equivalent of extends for structs?

Usually there is no direct equivalent. The idiomatic replacement is to store one struct inside another.

Can I extend a struct from an external crate?

You cannot add fields to the original struct, but you can wrap it inside your own struct and add your own fields there.

Should I use traits instead of inheritance in Rust?

Use traits when you want shared behavior. Use composition when you want to store shared data.

Why does Rust prefer composition over inheritance?

Composition is explicit, flexible, and works well with Rust's ownership and safety model.

Can I make nested fields easier to access?

Yes. Add methods on your outer struct that return or expose the inner data.

Is duplicating all fields from the original struct a good idea?

Usually no. It creates maintenance problems and can drift from the original type definition.

What if I want my type to behave like the inner type?

Wrap the inner type and implement methods or traits that delegate to it.

Take Quiz

Mini Project

Description

Build a small Rust program that models a student profile by wrapping an existing Person struct inside a Student struct. This demonstrates the Rust way to reuse data from one type while adding your own fields and methods.

Goal

Create a Student type that contains a Person, adds school-specific data, and exposes convenient methods without using inheritance.

Requirements

Create a Person struct with age and gender fields. Create a Student struct that contains a Person and adds a name and grade field. Implement a constructor method for Student. Add methods that return the student's age and name. Print a formatted summary of a student in main.

Take Quiz

Keep learning

Related questions

View more