How to Use the Self Type in Rust

Use Self in Rust impl blocks as a shorthand for the current type to simplify method signatures and return types.

The rename nightmare

You spend twenty minutes typing MyDatabaseConnectionPool across five methods. You write a constructor that returns MyDatabaseConnectionPool. You write a builder method that takes &MyDatabaseConnectionPool and returns MyDatabaseConnectionPool. Then you realize the name is too long. You rename the struct to Pool.

Now you have to hunt down every return type and argument. The compiler will catch the mistakes, but your brain has to do the search. You miss one spot, and the build breaks. Self exists to save you from this drudgery. Inside an impl block, Self is a type alias for the type you are implementing. It resolves to the type name at compile time. There is no runtime overhead. The compiler simply substitutes the name.

Concept: the compile-time mirror

Think of Self as a mirror the compiler holds up to the code. When the compiler sees Self inside an impl Rectangle block, it looks at the mirror and sees Rectangle. It replaces Self with Rectangle before generating machine code.

This aliasing works for the type itself, including all generic parameters and lifetimes. If you implement a trait for Wrapper<T, 'a>, Self resolves to Wrapper<T, 'a>. You never have to repeat the generic list. The alias tracks the full signature automatically.

Self is distinct from self. Self (capital S) is the type. self (lowercase s) is the instance value passed to the method. Confusing the two is a common typo, and the compiler catches it immediately.

Minimal example

Here is a struct with a constructor and a method that compares two instances. Both use Self to avoid repeating the type name.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    /// Creates a new Rectangle with the given dimensions.
    fn new(width: u32, height: u32) -> Self {
        // Self resolves to Rectangle.
        // Returning Self makes this method resilient to renames.
        Rectangle { width, height }
    }

    /// Checks if this rectangle can fully contain another.
    fn can_hold(&self, other: &Self) -> bool {
        // &Self is equivalent to &Rectangle.
        // Using Self keeps the signature consistent with the impl block.
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let big = Rectangle::new(10, 10);
    let small = Rectangle::new(5, 5);

    // Self is gone at runtime. These calls use the resolved types.
    println!("Can hold: {}", big.can_hold(&small));
}

Refactor with confidence. Self tracks the name for you.

The hidden sugar: &self is self: &Self

There is a connection between Self and the receiver argument that often goes unnoticed. When you write fn method(&self), you are using syntax sugar. The compiler expands this to fn method(self: &Self).

This means &self is literally a reference to Self. If you ever need to be explicit about the type of the receiver, you can write it out. This is rarely necessary in practice, but it clarifies why Self appears in the type signature even when you only typed &self.

struct Counter(u32);

impl Counter {
    /// Increments the counter and returns a mutable reference to itself.
    fn increment(&mut self) -> &mut Self {
        // &mut self expands to self: &mut Self.
        // The return type &mut Self matches the receiver type.
        self.0 += 1;
        self
    }
}

This pattern enables method chaining. The method returns a reference to the same type it operates on, allowing you to call another method immediately.

Self in traits: the implementor placeholder

Self becomes powerful inside traits. In a trait definition, Self does not refer to a specific struct. It refers to whatever type implements the trait. This allows traits to define methods that return the implementor itself.

The standard library uses this for Clone. The clone method must return a copy of the value. The only way to express "return a copy of whatever type is calling this" is with Self.

trait Clone {
    /// Returns a copy of the value.
    fn clone(&self) -> Self;
}

struct Point {
    x: f64,
    y: f64,
}

impl Clone for Point {
    fn clone(&self) -> Self {
        // Self resolves to Point inside this impl.
        Point { x: self.x, y: self.y }
    }
}

Convention aside: When calling a constructor from inside an impl block, use Self::new instead of Type::new. It keeps the code consistent with the return type and survives renames. The community treats Self::new as the standard way to construct the type within its own implementation.

Realistic example: the builder pattern

The builder pattern relies heavily on Self. A builder accumulates configuration and returns a reference to itself so you can chain calls. Self makes the builder generic over its own type without repeating the name.

struct ServerConfig {
    host: String,
    port: u16,
    debug: bool,
}

struct ServerBuilder {
    host: Option<String>,
    port: Option<u16>,
    debug: Option<bool>,
}

impl ServerBuilder {
    /// Creates a new builder with default values.
    fn new() -> Self {
        ServerBuilder {
            host: None,
            port: None,
            debug: None,
        }
    }

    /// Sets the host and returns the builder for chaining.
    fn host(&mut self, host: &str) -> &mut Self {
        self.host = Some(host.to_string());
        self
    }

    /// Sets the port and returns the builder for chaining.
    fn port(&mut self, port: u16) -> &mut Self {
        self.port = Some(port);
        self
    }

    /// Enables debug mode and returns the builder for chaining.
    fn debug(&mut self, enabled: bool) -> &mut Self {
        self.debug = Some(enabled);
        self
    }

    /// Builds the configuration, panicking if required fields are missing.
    fn build(&self) -> ServerConfig {
        ServerConfig {
            host: self.host.clone().expect("host is required"),
            port: self.port.expect("port is required"),
            debug: self.debug.unwrap_or(false),
        }
    }
}

fn main() {
    // Chaining works because each method returns &mut Self.
    let config = ServerBuilder::new()
        .host("localhost")
        .port(8080)
        .debug(true)
        .build();

    println!("Server: {}:{}", config.host, config.port);
}

The builder returns &mut Self from every setter. If you rename ServerBuilder, the return types update automatically. The chain stays valid.

Pitfalls and compiler errors

Self is simple, but a few traps exist.

Using Self outside an impl or trait block triggers E0433 (not found). The compiler has no context to resolve the alias. Self only exists where a type is being implemented.

// This fails.
// error[E0433]: failed to resolve: could not find `Self` in this scope
fn bad_function() -> Self {
    // ...
}

In traits, Self binds tightly to the implementor. If a trait method returns Self, the trait becomes object-unsafe. You cannot create a dyn Trait pointer for it. The compiler needs to know the exact size of the type at compile time, and Self forces that knowledge. If you need dynamic dispatch, you must return a trait object or use an associated type instead.

trait BadTrait {
    // This method returns Self.
    fn get_self(&self) -> Self;
}

struct MyType;
impl BadTrait for MyType {
    fn get_self(&self) -> Self {
        MyType
    }
}

fn main() {
    // This fails with a trait object safety error.
    // The compiler cannot create a vtable for a method that returns Self.
    let _ptr: &dyn BadTrait = &MyType;
}

The compiler rejects this with a trait object safety error. The message explains that Self cannot be used in trait objects. Fix this by returning Box<dyn BadTrait> or an associated type if the return type is related but distinct.

Another pitfall involves lifetimes. Self carries lifetimes automatically, but you must ensure the lifetime elision rules work in your favor. If Self contains a reference, returning Self might require explicit lifetime annotations. The compiler will guide you with E0106 (missing lifetime specifier) if the inference fails.

Decision: when to use Self

Use Self in impl blocks for constructors and methods returning the type. It reduces repetition and makes refactoring safe. Use Self in traits for methods returning the implementor. It allows traits like Clone to work for any type. Reach for associated types when you need a related type that differs from the implementor. Self is the type itself; associated types are companions. Pick generics when the return type varies per call. Self is fixed for the implementor; generics allow flexibility across calls.

Trust Self to keep your code consistent. It is the compiler's way of saying, "I know what type this is. You don't need to repeat yourself."

Where to go next