Advanced Types

Rust's type system has a few corners that don't come up in beginner code but explain things you've quietly wondered about — why some functions "return" a type that isn't really a type, why `str` behaves differently from other types, and how to give a complicated type a readable name. These advanced type features are small individually but together deepen your understanding of how Rust's type system actually works, which pays off when reading real code and library internals.

Continuing Module 4’s advanced features, this post covers advanced types: type aliases (giving types readable names), the never type !, and dynamically sized types and the Sized trait. These are lesser-known corners of Rust’s type system that explain real behavior and appear in serious code. Together with advanced traits (the previous post), they round out your understanding of Rust’s type system. None is essential daily, but each clarifies how Rust works.

Type aliases

A type alias gives an existing type a new name — reducing repetition and improving readability for complicated types:

// A type alias: Kilometers is another name for i32.
type Kilometers = i32;

// More useful: a readable name for a verbose type.
type Thunk = Box<dyn Fn() + Send + 'static>;

fn takes_long_type(f: Thunk) { /* ... */ }
fn returns_long_type() -> Thunk { Box::new(|| println!("hi")) }

fn main() {
    let x: i32 = 5;
    let y: Kilometers = 5;
    // Kilometers IS i32 — they're interchangeable (unlike a newtype).
    println!("{}", x + y); // 10
}

A type alias (type Name = ExistingType) gives an existing type a new name (a synonym, interchangeable — unlike a distinct newtype) — mainly valuable for readability with verbose, repeated types (like a long boxed closure type), not for type safety. A stranger corner is the never type.

The never type

The never type ! is a special type representing computations that never return — it enables things you’ve relied on without knowing:

fn main() {
    let values = vec!["1", "two", "3"];
    for v in values {
        // parse() returns Result; the Err arm uses `continue` (type `!`),
        // which coerces to match the Ok arm's type (u32). Without `!` coercion,
        // the match arms would have mismatched types.
        let num: u32 = match v.parse() {
            Ok(n) => n,
            Err(_) => continue, // type `!`, coerces to u32
        };
        println!("{num}");
    }
}

The never type ! (a type with no values, for computations that never return — panics, infinite loops, continue) coerces to any type, which is why match arms can continue/panic!/return (they have type ! that fits the other arms’ type) — a mostly-invisible feature you rely on constantly. Another type-system corner explains str and trait objects.

Dynamically sized types and Sized

Dynamically sized types (DSTs) are types whose size isn’t known at compile time — and the Sized trait, which is automatic, governs how Rust handles them:

Dynamically sized types (like str and dyn Trait) have runtime-known sizes, so they must be used behind (fat) pointers (&str, Box<dyn Trait>) — which is why you always see those forms — and the automatic Sized marker trait (implicitly required on generics) marks known-size types, with ?Sized relaxing it to accept DSTs. Advanced types — type aliases (readable names), the never type ! (diverging computations, match-arm coercion), and DSTs/Sized (variable-size types behind fat pointers) — are lesser-known corners that explain real Rust behavior. Next: unsafe Rust.

Key takeaways

Further reading

Sources & References

Type aliases, the never type, DSTs