DEV Community

Cover image for Advance Types In Rust
Sureni Dhanasekara
Sureni Dhanasekara

Posted on

Advance Types In Rust

Rust is known for its powerful type system, which enables memory safety, concurrency, and zero-cost abstractions. Rust basic types like integers, boolean, and strings are straightforward, advanced types provide the foundation for writing expressive and safe code in complex systems.

Most important advanced types in Rust:

  • Algebraic Data Types (Enums & Structs)
  • Type Aliases
  • Newtypes
  • PhantomData
  • Dynamically Sized Types (DSTs)
  • Trait Objects
  • Function Pointers
  • Never Type (!)
  • Inferred and Generic Types

Algebraic Data Types (ADTs)

Algebraic data types is a way to define complex data by combining simpler types.

There are two main kinds of ADTs:

  • Struct : combine multiple fields together

        struct Person {
          name: String,
          age: u8,
        }
    
  • Enum : define one of many possibilities

        enum Shape {
          Circle(f64),
          Rectangle(f64, f64),
       }
    

Newtypes

A newtype is just a tuple struct with one field which is used to create a new, distinct type from an existing type.

struct MyType(i32); 

Enter fullscreen mode Exit fullscreen mode

Why Use Newtypes:

  • Type safety :two types with the same inner value not interchangeable, so you can’t accidentally pass Meters when Millimeters is expected.
struct Millimeters(u32);
struct Meters(u32);

Enter fullscreen mode Exit fullscreen mode

Example without newtype:

fn print_length(length_mm: u32) {
    println!("Length is {} millimeters", length_mm);
}

let meters = 5; 
print_length(meters); 
Enter fullscreen mode Exit fullscreen mode

If we passed 5 meters where the function expected millimeters. But Rust doesn’t catch this, because they’re both just u32.

Example with newtype:

struct Millimeters(u32);
struct Meters(u32);

fn print_length(length_mm: Millimeters) {
       println!("Length is {} millimeters", length_mm.0);
}

let meters = Meters(5);
print_length(meters); // ❌ ERROR!
Enter fullscreen mode Exit fullscreen mode

If we run this then you will get a compile-time error:

expected struct `Millimeters`, found struct `Meters`
Enter fullscreen mode Exit fullscreen mode
  • Custom Trait Implementations: Rust doesn't let you implement foreign traits for foreign types that means you can't implement a trait like Display for a built-in type like Vec unless you own either the trait or the type. We can solve this issue with newtype.
use std::fmt;

struct CommaSeparated(Vec<String>);

impl fmt::Display for CommaSeparated {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0.join(", "))
    }
}

fn main() {
    let names = CommaSeparated(vec!["Alice".into(), "Bob".into(), "Carol".into()]);
    println!("{}", names); // prints: Alice, Bob, Carol
}
Enter fullscreen mode Exit fullscreen mode
  • Encapsulation: Encapsulation means hiding internal details of a type and exposing only what’s necessary.

It’s a core idea in programming that helps:

  • Prevent misuse
  • Keep your codebase clean
  • Make changes without breaking other code

In Rust, this is done using:

  • Private fields
  • Public methods (getters/setters or controlled logic)
pub struct Password(String); // type is public, field is private

impl Password {
    pub fn new(s: &str) -> Self {
        Password(s.to_string())
    }

    pub fn value(&self) -> &str {
        &self.0
    }
}
Enter fullscreen mode Exit fullscreen mode

Type Aliases

A type alias in Rust lets you create a new name for an existing type - but it doesn't create a new, distinct type.

type Kilometers = i32;

fn main() {
    let distance: Kilometers = 5;
    let total: i32 = distance + 10; // Works — because Kilometers is just an alias for i32
}
Enter fullscreen mode Exit fullscreen mode

PhantomData

PhantomData is a zero-sized marker type in Rust that's used to indicate that a type or lifetime parameter is logically "used" by your type, even though it doesn't actually contain any data of that type.

Key Properties:

  • Zero-sized (no runtime overhead)
  • Only used at compile time for type system purposes
  • Doesn't affect runtime behavior
  • Can be used with any combination of type and lifetime parameters

Dynamically Sized Types (DSTs)

In Rust, most types must have a known size at compile time. But DSTs are types whose size is only known at runtime.

Key Characteristics of DSTs :
- Size unknown at compile time
- Must always be used behind a pointer (&, Box, Rc, etc.)

- Don't implement the Sized trait

Common DSTs in Rust:

  • Slices ([T])
let slice: &[i32] = &[1, 2, 3];  // DST - size depends on runtime length
Enter fullscreen mode Exit fullscreen mode
  • Trait Objects (dyn Trait)
let trait_obj: &dyn Display = &42;  // DST - size depends on concrete type
Enter fullscreen mode Exit fullscreen mode
  • Structs with DST as last field
struct MyString {
    len: usize,
    data: [u8],  // DST as last field
}
Enter fullscreen mode Exit fullscreen mode

Never Type (!)

The ! type in Rust means "this function never returns".

Key Characteristics:

  • Diverging functions: Functions that never return have return type !
  • Zero variants: The ! type has no possible values
  • Subtype of all types: Can be coerced into any other type
  • No runtime representation: Doesn't exist at runtime.

Examples:

  • A Function That Panics - The return type is ! because this function never returns normally it always panics.
fn crash() -> ! {
    panic!("Something went terribly wrong");
}
Enter fullscreen mode Exit fullscreen mode
  • An Infinite Loop
fn loop_forever() -> ! {
    loop {
        // runs forever
    }
}
Enter fullscreen mode Exit fullscreen mode

Function Pointers

Function pointers in Rust allow you to store references to functions and pass them around like regular values. They're a fundamental building block for many advanced patterns like callbacks, plugins, and strategy patterns.

Basic Function Pointer Syntax:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    // Create a function pointer
    let operation: fn(i32, i32) -> i32 = add;

    // Use the function pointer
    let result = operation(2, 3);
    println!("Result: {}", result); // Prints "5"
}
Enter fullscreen mode Exit fullscreen mode

Key Characteristics:

  • Type Signature: Function pointers have the type fn(T1, T2, ...) -> R where Tn are argument types and R is the return type
  • Zero-Sized: They don't store any data, just point to code
  • Safe: Unlike raw pointers, function pointers are guaranteed to point to valid functions
  • No Capture: They can only point to functions, not closures that capture environment

Top comments (1)

Collapse
 
jonesbeach profile image
Jones Beach

you covered so much here! I've heard of all these but only used about half or so myself. are you building anything with these?