home / skills / huiali / rust-skills / rust-type-driven

rust-type-driven skill

/skills/rust-type-driven

This skill applies type-driven Rust patterns like newtype, type state, PhantomData, and builders to prevent errors and improve safety across domains.

npx playbooks add skill huiali/rust-skills --skill rust-type-driven

Review the files below or copy the command above to add this skill to your agents.

Files (4)
SKILL.md
9.1 KB
---
name: rust-type-driven
description: Type-driven design expert covering newtype pattern, type state, PhantomData, marker traits, builder pattern, compile-time validation, sealed traits, and zero-sized types (ZST).
metadata:
  triggers:
    - type-driven
    - newtype
    - type state
    - PhantomData
    - marker trait
    - builder pattern
    - compile-time validation
    - sealed trait
    - ZST
    - zero-sized type
---


## Solution Patterns

### Pattern 1: Newtype Pattern

```rust
// ❌ Primitive types can be confused
fn process_user(id: u64) { ... }
fn process_order(id: u64) { ... }

// Easy to mix up:
process_order(user_id);  // Compiles but wrong!

// ✅ Type-safe newtypes
struct UserId(u64);
struct OrderId(u64);

fn process_user(id: UserId) { ... }
fn process_order(id: OrderId) { ... }

// Compiler prevents:
// process_order(user_id);  // Compile error!
```

**When to use:**
- Domain-specific identifiers
- Values with different semantics but same representation
- Adding type-level validation

### Pattern 2: Type State Pattern

```rust
// Encode states in types
struct Disconnected;
struct Connecting;
struct Connected;

struct Connection<State = Disconnected> {
    socket: TcpSocket,
    _state: PhantomData<State>,
}

impl Connection<Disconnected> {
    pub fn new() -> Self {
        Connection {
            socket: TcpSocket::new(),
            _state: PhantomData,
        }
    }

    pub fn connect(self) -> Connection<Connecting> {
        // Start connection...
        Connection {
            socket: self.socket,
            _state: PhantomData,
        }
    }
}

impl Connection<Connecting> {
    pub fn finish(self) -> Result<Connection<Connected>, Error> {
        // Complete connection...
        Ok(Connection {
            socket: self.socket,
            _state: PhantomData,
        })
    }
}

impl Connection<Connected> {
    pub fn send(&mut self, data: &[u8]) -> Result<(), Error> {
        // Only Connected state can send
        self.socket.write(data)
    }
}

// Type state prevents invalid operations:
let conn = Connection::new();
// conn.send(data);  // Compile error! Not connected yet
let conn = conn.connect();
let mut conn = conn.finish()?;
conn.send(data)?;  // OK!
```

### Pattern 3: PhantomData for Ownership

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

// PhantomData marks ownership and variance
struct MyIterator<'a, T> {
    ptr: *const T,
    end: *const T,
    _marker: PhantomData<&'a T>,  // Tells compiler: we borrow T
}

// Without PhantomData, compiler doesn't know about the 'a lifetime
```

### Pattern 4: Builder Pattern with Type State

```rust
// Type-safe builder that enforces required fields
struct HostSet;
struct HostUnset;
struct PortSet;
struct PortUnset;

struct ConfigBuilder<H, P> {
    host: Option<String>,
    port: Option<u16>,
    _host: PhantomData<H>,
    _port: PhantomData<P>,
}

impl ConfigBuilder<HostUnset, PortUnset> {
    pub fn new() -> Self {
        ConfigBuilder {
            host: None,
            port: None,
            _host: PhantomData,
            _port: PhantomData,
        }
    }
}

impl<P> ConfigBuilder<HostUnset, P> {
    pub fn host(self, host: impl Into<String>) -> ConfigBuilder<HostSet, P> {
        ConfigBuilder {
            host: Some(host.into()),
            port: self.port,
            _host: PhantomData,
            _port: PhantomData,
        }
    }
}

impl<H> ConfigBuilder<H, PortUnset> {
    pub fn port(self, port: u16) -> ConfigBuilder<H, PortSet> {
        ConfigBuilder {
            host: self.host,
            port: Some(port),
            _host: PhantomData,
            _port: PhantomData,
        }
    }
}

// Only works when both required fields are set
impl ConfigBuilder<HostSet, PortSet> {
    pub fn build(self) -> Config {
        Config {
            host: self.host.unwrap(),
            port: self.port.unwrap(),
        }
    }
}

// Usage:
let config = ConfigBuilder::new()
    .host("localhost")
    .port(8080)
    .build();  // OK

// Won't compile without required fields:
// ConfigBuilder::new().build();  // Error!
```


## Making Invalid States Unrepresentable

```rust
// ❌ Easy to create invalid state
struct User {
    name: String,
    email: Option<String>,  // Might be empty
    age: u32,
}

// ✅ Email cannot be invalid
struct User {
    name: String,
    email: Email,  // Type guarantees validity
    age: u32,
}

struct Email(String);

impl Email {
    pub fn new(s: impl Into<String>) -> Result<Self, EmailError> {
        let s = s.into();
        if s.contains('@') && s.len() > 3 {
            Ok(Email(s))
        } else {
            Err(EmailError::Invalid)
        }
    }

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


## Marker Traits

```rust
// Use marker traits to signal capabilities
trait Sendable: Send + 'static {}

// Sealed trait pattern (prevent external implementation)
mod sealed {
    pub trait Sealed {}
}

pub trait MyTrait: sealed::Sealed {
    fn method(&self);
}

// Only types we define can implement MyTrait
struct MyType;
impl sealed::Sealed for MyType {}
impl MyTrait for MyType {
    fn method(&self) { ... }
}
```


## Zero-Sized Types (ZST)

```rust
// Use ZST for compile-time markers (no runtime cost)
struct DebugOnly;
struct Always;

struct Logger<Mode = Always> {
    _marker: PhantomData<Mode>,
}

impl Logger<DebugOnly> {
    pub fn log(&self, msg: &str) {
        #[cfg(debug_assertions)]
        println!("[DEBUG] {}", msg);
    }
}

impl Logger<Always> {
    pub fn log(&self, msg: &str) {
        println!("[LOG] {}", msg);
    }
}

// ZST has zero runtime cost:
assert_eq!(std::mem::size_of::<Logger<DebugOnly>>(), 0);
```


## Workflow

### Step 1: Identify Domain Invariants

```
What can go wrong?
  → IDs mixed up? Use newtype
  → Invalid state transitions? Use type state
  → Optional fields always present? Remove Option
  → Values need validation? Validate in constructor
```

### Step 2: Choose Type Pattern

```
Need to:
  → Prevent ID confusion? Newtype pattern
  → Encode state machine? Type state pattern
  → Enforce required fields? Builder with type state
  → Mark variance/ownership? PhantomData
  → Zero-cost abstraction? ZST
```

### Step 3: Validate at Construction

```rust
// ✅ Validation at construction
impl Email {
    pub fn new(s: &str) -> Result<Self, Error> {
        validate(s)?;  // Validate once
        Ok(Email(s.to_string()))
    }
}

// Now Email is always valid
fn send_email(to: Email) {
    // No need to re-validate
}
```


## Anti-Patterns

| Anti-Pattern | Problem | Solution |
|--------------|---------|----------|
| `is_valid` flag | Runtime checking | Use type states |
| Many `Option`s | Nullable everywhere | Redesign types |
| Primitive types everywhere | Type confusion | Newtype pattern |
| Runtime validation | Late error discovery | Constructor validation |
| Boolean parameters | Unclear meaning | Use enum or builder |


## Validation Timing

| Validation Type | Best Time | Example |
|-----------------|-----------|---------|
| Range validation | Construction | `Email::new()` returns `Result` |
| State transitions | Type boundaries | `Connection<Connected>` |
| Reference validity | Lifetimes | `&'a T` |
| Thread safety | `Send + Sync` | Compiler checks |


## Review Checklist

When reviewing type design:

- [ ] Invalid states are unrepresentable
- [ ] Newtypes used for domain concepts
- [ ] Validation happens at construction
- [ ] Type states prevent invalid operations
- [ ] No boolean blindness (use enums)
- [ ] PhantomData correctly marks ownership
- [ ] Builder enforces required fields
- [ ] Marker traits document capabilities
- [ ] ZSTs used for zero-cost abstractions


## Verification Commands

```bash
# Check type sizes
cargo build --release
nm target/release/myapp | grep MyType

# Ensure ZST optimization
objdump -d target/release/myapp | grep -A 10 my_function

# Test type-level guarantees
cargo test --lib
```


## Common Pitfalls

### 1. Boolean Blindness

**Symptom**: Unclear what true/false means

```rust
// ❌ Bad: what does true mean?
fn connect(hostname: &str, secure: bool) { ... }

// ✅ Good: explicit type
enum ConnectionMode {
    Secure,
    Insecure,
}

fn connect(hostname: &str, mode: ConnectionMode) { ... }
```

### 2. Optional Fields That Shouldn't Be

**Symptom**: Lots of `Option` everywhere

```rust
// ❌ Bad: user email should always exist
struct User {
    name: String,
    email: Option<String>,
}

// ✅ Good: validate at construction
struct User {
    name: String,
    email: Email,  // Always valid
}
```

### 3. Missing Newtype

**Symptom**: Mixing up IDs

```rust
// ❌ Bad: easy to confuse
fn transfer_money(from: u64, to: u64, amount: u64) { ... }

// transfer_money(amount, to, from);  // Oops!

// ✅ Good: type safety
fn transfer_money(from: AccountId, to: AccountId, amount: Money) { ... }
```


## Related Skills

- **rust-ownership** - Lifetime and borrowing fundamentals
- **rust-trait** - Advanced trait patterns
- **rust-pattern** - Design pattern implementations
- **rust-zero-cost** - Zero-cost abstractions
- **rust-linear-type** - Linear types and session types


## Localized Reference

- **Chinese version**: [SKILL_ZH.md](./SKILL_ZH.md) - 完整中文版本,包含所有内容

Overview

This skill is a type-driven design expert for Rust that explains and applies patterns like newtypes, type state, PhantomData, marker traits, builders, sealed traits, and zero-sized types (ZST). It focuses on making invalid states unrepresentable, shifting validation to construction, and using the type system to prevent runtime errors. The guidance is practical and aimed at improving correctness, readability, and zero-cost abstractions in real projects.

How this skill works

The skill inspects domain invariants and recommends the appropriate type pattern (newtype, type state, ZST, builder with type-state, PhantomData, or marker/sealed traits). It provides concrete examples and transformation steps: replace primitives with newtypes, encode states in type parameters, validate at construction, and use PhantomData or ZSTs for ownership and compile-time markers. It also highlights anti-patterns and a short review checklist to verify type-level guarantees.

When to use it

  • When IDs or semantically different values share the same primitive type (use newtype).
  • When an API has invalid or illegal transitions you want caught at compile time (use type state).
  • When you must enforce required builder fields before construction (builder with type-state).
  • When you need to express ownership/variance or lifetimes without runtime data (use PhantomData).
  • When you want zero-cost compile-time markers or mode selection (use ZSTs).

Best practices

  • Validate once at construction and return Result to make valid values unrepresentable later.
  • Prefer newtypes over raw primitives for domain concepts and units to avoid mix-ups.
  • Encode state machines in types so only valid operations compile; keep transitions explicit and small.
  • Use PhantomData to declare ownership, variance, or lifetimes for ZST wrappers and iterators.
  • Use marker and sealed traits to document capabilities and prevent external implementations while keeping APIs extensible.

Example use cases

  • User and order identifiers implemented as distinct newtypes to prevent ID mixups.
  • A network Connection<State> API that only allows send() on Connection<Connected>.
  • ConfigBuilder that enforces host and port are set at compile time using type-state markers.
  • Email type with Email::new() that validates format once and guarantees downstream callers a valid email.
  • Logger<DebugOnly> and Logger<Always> as ZST-backed modes to toggle behavior without runtime cost.

FAQ

When should I prefer a builder with type state over runtime checks?

Use a type-state builder when missing fields are a compile-time correctness issue; it prevents whole classes of runtime errors and documents required fields explicitly.

Does PhantomData add runtime cost?

No. PhantomData is zero-sized and exists only for the compiler to track lifetimes, variance, or ownership; it has no runtime footprint when used correctly.