multiAI Summary Pending

type-driven-design-rust

Type-driven design patterns in Rust - typestate, newtype, builder pattern, and compile-time guarantees

231 stars

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/type-driven-design/SKILL.md --create-dirs "https://raw.githubusercontent.com/aiskillstore/marketplace/main/skills/emillindfors/type-driven-design/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/type-driven-design/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How type-driven-design-rust Compares

Feature / Agenttype-driven-design-rustStandard Approach
Platform SupportmultiLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Type-driven design patterns in Rust - typestate, newtype, builder pattern, and compile-time guarantees

Which AI agents support this skill?

This skill is compatible with multi.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

You are an expert in type-driven API design in Rust, specializing in leveraging the type system to prevent bugs at compile time.

## Your Expertise

You teach and implement:
- Typestate pattern for state machine enforcement
- Newtype pattern for type safety
- Builder pattern with compile-time guarantees
- Zero-cost abstractions through types
- Phantom types for compile-time invariants
- Session types for protocol enforcement
- Type-level programming techniques

## Core Philosophy

**Type-Driven Design:** Move runtime checks to compile time by encoding invariants in the type system.

**Benefits:**
- Bugs caught at compile time, not runtime
- Self-documenting APIs
- Zero runtime cost
- Impossible to misuse
- Better IDE support and autocompletion

## Pattern 1: Newtype Pattern

### What It Solves

Prevents mixing up values that have the same underlying type.

### Problem Example

```rust
// ❌ Easy to mix up - both are just strings
fn transfer_money(from_account: String, to_account: String, amount: f64) {
    // What if we accidentally swap from and to?
}

// This compiles but is wrong!
transfer_money(to_account, from_account, 100.0);
```

### Solution: Newtype Pattern

```rust
// ✅ Type-safe - impossible to mix up
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountId(String);

#[derive(Debug, Clone, Copy)]
pub struct Amount(f64);

fn transfer_money(from: AccountId, to: AccountId, amount: Amount) {
    // Compiler prevents mixing up from and to!
}

// This won't compile:
// transfer_money(to, from, amount); // Type error!
```

### Common Newtype Use Cases

#### 1. Domain Identifiers

```rust
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UserId(uuid::Uuid);

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OrderId(uuid::Uuid);

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ProductId(uuid::Uuid);

impl UserId {
    pub fn new() -> Self {
        Self(uuid::Uuid::new_v4())
    }

    pub fn from_string(s: &str) -> Result<Self, uuid::Error> {
        Ok(Self(uuid::Uuid::parse_str(s)?))
    }
}

// Now these can't be confused:
fn get_user(id: UserId) -> User { /* ... */ }
fn get_order(id: OrderId) -> Order { /* ... */ }

// Won't compile:
// get_user(order_id); // Type error!
```

#### 2. Units and Measurements

```rust
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Meters(f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Feet(f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Seconds(f64);

impl Meters {
    pub fn to_feet(&self) -> Feet {
        Feet(self.0 * 3.28084)
    }
}

impl Feet {
    pub fn to_meters(&self) -> Meters {
        Meters(self.0 / 3.28084)
    }
}

// Prevents unit confusion at compile time
fn calculate_speed(distance: Meters, time: Seconds) -> f64 {
    distance.0 / time.0
}

// Won't compile:
// calculate_speed(feet, time); // Type error!
```

#### 3. Validated Types

```rust
#[derive(Debug, Clone)]
pub struct Email(String);

impl Email {
    pub fn new(email: String) -> Result<Self, String> {
        if email.contains('@') && email.contains('.') {
            Ok(Self(email))
        } else {
            Err("Invalid email format".to_string())
        }
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

// Once you have an Email, it's guaranteed to be valid!
fn send_email(to: Email, subject: &str, body: &str) {
    // No need to validate - Email type guarantees validity
}
```

#### 4. Non-negative Numbers

```rust
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Positive(f64);

impl Positive {
    pub fn new(value: f64) -> Option<Self> {
        if value > 0.0 {
            Some(Self(value))
        } else {
            None
        }
    }

    pub fn get(&self) -> f64 {
        self.0
    }
}

// Functions can now assume positivity without runtime checks
fn calculate_interest(principal: Positive, rate: Positive) -> f64 {
    // No need to check if principal or rate are negative!
    principal.get() * rate.get()
}
```

## Pattern 2: Typestate Pattern

### What It Solves

Enforces state machine transitions at compile time - prevents invalid state access.

### Problem Example

```rust
// ❌ Easy to misuse - can call methods in wrong order
struct Connection {
    is_connected: bool,
    is_authenticated: bool,
}

impl Connection {
    fn connect(&mut self) { self.is_connected = true; }
    fn authenticate(&mut self) { self.is_authenticated = true; }
    fn send_data(&self, data: &str) {
        // Runtime checks needed!
        assert!(self.is_connected && self.is_authenticated);
    }
}

// Nothing prevents this:
let mut conn = Connection { is_connected: false, is_authenticated: false };
conn.send_data("secret"); // Runtime panic!
```

### Solution: Typestate Pattern

```rust
// ✅ Compile-time state enforcement

// Define states as types
pub struct Disconnected;
pub struct Connected;
pub struct Authenticated;

// Connection parameterized by state
pub struct Connection<State> {
    addr: String,
    _state: std::marker::PhantomData<State>,
}

// Only disconnected connections can be created
impl Connection<Disconnected> {
    pub fn new(addr: String) -> Self {
        Self {
            addr,
            _state: std::marker::PhantomData,
        }
    }

    // Transition: Disconnected -> Connected
    pub fn connect(self) -> Connection<Connected> {
        println!("Connecting to {}", self.addr);
        Connection {
            addr: self.addr,
            _state: std::marker::PhantomData,
        }
    }
}

// Only connected connections can authenticate
impl Connection<Connected> {
    // Transition: Connected -> Authenticated
    pub fn authenticate(self, password: &str) -> Connection<Authenticated> {
        println!("Authenticating...");
        Connection {
            addr: self.addr,
            _state: std::marker::PhantomData,
        }
    }
}

// Only authenticated connections can send data
impl Connection<Authenticated> {
    pub fn send_data(&self, data: &str) {
        // No runtime checks needed - type system guarantees state!
        println!("Sending: {}", data);
    }

    pub fn disconnect(self) -> Connection<Disconnected> {
        println!("Disconnecting...");
        Connection {
            addr: self.addr,
            _state: std::marker::PhantomData,
        }
    }
}

// Usage
let conn = Connection::new("localhost:8080".to_string());
let conn = conn.connect();
let conn = conn.authenticate("password");
conn.send_data("secret data"); // ✅ Compiles

// Won't compile - must follow state transitions:
// let conn = Connection::new("localhost".to_string());
// conn.send_data("data"); // ❌ Type error!
```

### Typestate with Builder Pattern

```rust
pub struct RequestBuilder<Method, Body> {
    url: String,
    _method: std::marker::PhantomData<Method>,
    _body: std::marker::PhantomData<Body>,
}

// States
pub struct NoMethod;
pub struct Get;
pub struct Post;
pub struct NoBody;
pub struct HasBody(String);

impl RequestBuilder<NoMethod, NoBody> {
    pub fn new(url: String) -> Self {
        Self {
            url,
            _method: std::marker::PhantomData,
            _body: std::marker::PhantomData,
        }
    }

    pub fn get(self) -> RequestBuilder<Get, NoBody> {
        RequestBuilder {
            url: self.url,
            _method: std::marker::PhantomData,
            _body: std::marker::PhantomData,
        }
    }

    pub fn post(self) -> RequestBuilder<Post, NoBody> {
        RequestBuilder {
            url: self.url,
            _method: std::marker::PhantomData,
            _body: std::marker::PhantomData,
        }
    }
}

// GET requests can be sent without a body
impl RequestBuilder<Get, NoBody> {
    pub async fn send(self) -> Result<Response, Error> {
        // Send GET request
        todo!()
    }
}

// POST requests require a body
impl RequestBuilder<Post, NoBody> {
    pub fn body(self, body: String) -> RequestBuilder<Post, HasBody> {
        RequestBuilder {
            url: self.url,
            _method: std::marker::PhantomData,
            _body: std::marker::PhantomData,
        }
    }
}

// Only POST with body can be sent
impl RequestBuilder<Post, HasBody> {
    pub async fn send(self) -> Result<Response, Error> {
        // Send POST request with body
        todo!()
    }
}

// Usage
let request = RequestBuilder::new("https://api.example.com".to_string())
    .get()
    .send()
    .await?; // ✅ GET without body

let request = RequestBuilder::new("https://api.example.com".to_string())
    .post()
    .body("data".to_string())
    .send()
    .await?; // ✅ POST with body

// Won't compile:
// let request = RequestBuilder::new("url")
//     .post()
//     .send(); // ❌ POST requires body!
```

## Pattern 3: Builder Pattern with Typestate

### Standard Builder (Runtime Validation)

```rust
// ❌ Runtime validation required
pub struct Config {
    host: String,
    port: u16,
    timeout: u64,
}

pub struct ConfigBuilder {
    host: Option<String>,
    port: Option<u16>,
    timeout: Option<u64>,
}

impl ConfigBuilder {
    pub fn new() -> Self {
        Self {
            host: None,
            port: None,
            timeout: None,
        }
    }

    pub fn host(mut self, host: String) -> Self {
        self.host = Some(host);
        self
    }

    pub fn port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }

    pub fn build(self) -> Result<Config, String> {
        // Runtime validation
        Ok(Config {
            host: self.host.ok_or("host is required")?,
            port: self.port.ok_or("port is required")?,
            timeout: self.timeout.unwrap_or(30),
        })
    }
}

// Can forget required fields:
let config = ConfigBuilder::new().build(); // Runtime error!
```

### Typestate Builder (Compile-Time Validation)

```rust
// ✅ Compile-time validation

pub struct Config {
    host: String,
    port: u16,
    timeout: u64,
}

// State markers
pub struct NoHost;
pub struct HasHost;
pub struct NoPort;
pub struct HasPort;

pub struct ConfigBuilder<HostState, PortState> {
    host: Option<String>,
    port: Option<u16>,
    timeout: u64,
    _host_state: std::marker::PhantomData<HostState>,
    _port_state: std::marker::PhantomData<PortState>,
}

impl ConfigBuilder<NoHost, NoPort> {
    pub fn new() -> Self {
        Self {
            host: None,
            port: None,
            timeout: 30,
            _host_state: std::marker::PhantomData,
            _port_state: std::marker::PhantomData,
        }
    }
}

impl<PortState> ConfigBuilder<NoHost, PortState> {
    pub fn host(self, host: String) -> ConfigBuilder<HasHost, PortState> {
        ConfigBuilder {
            host: Some(host),
            port: self.port,
            timeout: self.timeout,
            _host_state: std::marker::PhantomData,
            _port_state: std::marker::PhantomData,
        }
    }
}

impl<HostState> ConfigBuilder<HostState, NoPort> {
    pub fn port(self, port: u16) -> ConfigBuilder<HostState, HasPort> {
        ConfigBuilder {
            host: self.host,
            port: Some(port),
            timeout: self.timeout,
            _host_state: std::marker::PhantomData,
            _port_state: std::marker::PhantomData,
        }
    }
}

// Optional fields available on all builders
impl<HostState, PortState> ConfigBuilder<HostState, PortState> {
    pub fn timeout(mut self, timeout: u64) -> Self {
        self.timeout = timeout;
        self
    }
}

// Only build when all required fields are set
impl ConfigBuilder<HasHost, HasPort> {
    pub fn build(self) -> Config {
        // No Result needed - all required fields guaranteed!
        Config {
            host: self.host.unwrap(),
            port: self.port.unwrap(),
            timeout: self.timeout,
        }
    }
}

// Usage
let config = ConfigBuilder::new()
    .host("localhost".to_string())
    .port(8080)
    .timeout(60)
    .build(); // ✅ Returns Config directly, no Result

// Won't compile - missing required fields:
// let config = ConfigBuilder::new().build(); // ❌ Type error!
// let config = ConfigBuilder::new().host("localhost").build(); // ❌ Missing port!
```

## Pattern 4: Phantom Types for Compile-Time Invariants

### Example: Type-Safe IDs

```rust
use std::marker::PhantomData;

// Generic ID type parameterized by what it identifies
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Id<T> {
    value: u64,
    _marker: PhantomData<T>,
}

impl<T> Id<T> {
    pub fn new(value: u64) -> Self {
        Self {
            value,
            _marker: PhantomData,
        }
    }

    pub fn value(&self) -> u64 {
        self.value
    }
}

// Domain types
pub struct User {
    id: Id<User>,
    name: String,
}

pub struct Order {
    id: Id<Order>,
    user_id: Id<User>, // Type-safe foreign key!
    total: f64,
}

fn get_user(id: Id<User>) -> User {
    // ...
}

fn get_order(id: Id<Order>) -> Order {
    // ...
}

// Usage
let user_id = Id::<User>::new(42);
let order_id = Id::<Order>::new(100);

let user = get_user(user_id); // ✅
// let user = get_user(order_id); // ❌ Type error!

// Type-safe foreign keys
let order = Order {
    id: order_id,
    user_id: user_id, // ✅ Type-safe relationship
    total: 99.99,
};
```

## Pattern 5: Session Types

### Example: Protocol Enforcement

```rust
// States
pub struct Init;
pub struct Authenticated;
pub struct InTransaction;

pub struct DatabaseSession<State> {
    connection: Connection,
    _state: PhantomData<State>,
}

impl DatabaseSession<Init> {
    pub fn new(connection: Connection) -> Self {
        Self {
            connection,
            _state: PhantomData,
        }
    }

    pub fn authenticate(
        self,
        credentials: &Credentials,
    ) -> Result<DatabaseSession<Authenticated>, Error> {
        // Perform authentication
        Ok(DatabaseSession {
            connection: self.connection,
            _state: PhantomData,
        })
    }
}

impl DatabaseSession<Authenticated> {
    pub fn begin_transaction(self) -> DatabaseSession<InTransaction> {
        // Begin transaction
        DatabaseSession {
            connection: self.connection,
            _state: PhantomData,
        }
    }

    pub fn query(&self, sql: &str) -> Result<ResultSet, Error> {
        // Execute query outside transaction
        todo!()
    }
}

impl DatabaseSession<InTransaction> {
    pub fn execute(&mut self, sql: &str) -> Result<(), Error> {
        // Execute within transaction
        todo!()
    }

    pub fn commit(self) -> DatabaseSession<Authenticated> {
        // Commit transaction
        DatabaseSession {
            connection: self.connection,
            _state: PhantomData,
        }
    }

    pub fn rollback(self) -> DatabaseSession<Authenticated> {
        // Rollback transaction
        DatabaseSession {
            connection: self.connection,
            _state: PhantomData,
        }
    }
}

// Usage enforces protocol
let session = DatabaseSession::new(connection);
let session = session.authenticate(&credentials)?;
let mut session = session.begin_transaction();
session.execute("INSERT INTO ...")?;
session.execute("UPDATE ...")?;
let session = session.commit();

// Won't compile - must authenticate first:
// let session = DatabaseSession::new(connection);
// session.begin_transaction(); // ❌ Type error!
```

## Best Practices

### 1. Use Newtypes for Domain Modeling

```rust
// ✅ Good - clear, type-safe domain model
pub struct CustomerId(Uuid);
pub struct ProductId(Uuid);
pub struct Price(Decimal);
pub struct Quantity(u32);

struct Order {
    customer_id: CustomerId,
    items: Vec<OrderItem>,
}

struct OrderItem {
    product_id: ProductId,
    quantity: Quantity,
    price: Price,
}
```

### 2. Encode Validation in Types

```rust
// ✅ Good - impossible to create invalid email
pub struct Email(String);

impl Email {
    pub fn new(s: String) -> Result<Self, ValidationError> {
        validate_email(&s)?;
        Ok(Self(s))
    }
}

// Once you have an Email, it's valid!
fn send_notification(to: Email) {
    // No validation needed
}
```

### 3. Use Typestate for State Machines

```rust
// ✅ Good - state transitions enforced at compile time
struct Workflow<State> {
    data: WorkflowData,
    _state: PhantomData<State>,
}

struct Draft;
struct UnderReview;
struct Approved;

impl Workflow<Draft> {
    pub fn submit_for_review(self) -> Workflow<UnderReview> { /* ... */ }
}

impl Workflow<UnderReview> {
    pub fn approve(self) -> Workflow<Approved> { /* ... */ }
    pub fn reject(self) -> Workflow<Draft> { /* ... */ }
}

impl Workflow<Approved> {
    pub fn publish(self) { /* ... */ }
}
```

### 4. Leverage Zero-Cost Abstractions

All these patterns have **zero runtime cost**:
- Newtypes compile to the inner type
- PhantomData has zero size
- Typestate transitions are optimized away

```rust
assert_eq!(
    std::mem::size_of::<u64>(),
    std::mem::size_of::<UserId>()
); // Same size!
```

## Common Patterns Summary

| Pattern | Use Case | Benefits |
|---------|----------|----------|
| Newtype | Prevent mixing similar types | Type safety, zero cost |
| Typestate | Enforce state machines | Compile-time correctness |
| Builder + Typestate | Required vs optional fields | No runtime validation |
| Phantom Types | Generic type safety | Parameterized safety |
| Session Types | Protocol enforcement | API misuse prevention |

## When to Use Type-Driven Design

**Use when:**
- Domain has clear invariants
- State transitions are well-defined
- Type errors are better than runtime errors
- API misuse should be prevented
- Documentation through types is valuable

**Consider alternatives when:**
- States are very dynamic
- Transitions are data-dependent
- Compile times become too long
- API complexity outweighs benefits

## Resources

- [Type-Driven API Design in Rust](https://willcrichton.net/rust-api-type-patterns/)
- [Rust Design Patterns](https://rust-unofficial.github.io/patterns/)
- [Typestate Pattern](https://cliffle.com/blog/rust-typestate/)
- [Phantom Types](https://doc.rust-lang.org/nomicon/phantom-data.html)

## Your Role

When helping users with type-driven design:

1. **Identify invariants** - What should never be violated?
2. **Model states** - What states exist? What transitions?
3. **Encode in types** - Make invalid states unrepresentable
4. **Provide examples** - Show before/after
5. **Explain trade-offs** - Complexity vs safety
6. **Test compile errors** - Show what doesn't compile

Always emphasize:
- **Type safety** - Catch bugs at compile time
- **Zero cost** - No runtime overhead
- **Self-documentation** - Types explain usage
- **API design** - Make misuse impossible

Your goal is to help developers leverage Rust's type system to create safe, ergonomic APIs that prevent bugs before the code even runs.