m05-type-driven
actionbook/rust-skills
Use Rust's type system to make invalid states unrepresentable at compile time.
What is m05-type-driven?
Type-driven design leverages Rust's type system to encode invariants and prevent invalid states before runtime. Use this when you need to validate data, manage state transitions, or add semantic meaning to primitives—asking "can the compiler catch this?" before reaching for runtime checks.
- Encode invariants as types (newtype, type state, marker traits) to prevent invalid states
- Use PhantomData to track type parameters for variance and lifetime control without runtime cost
- Implement builder patterns for gradual, validated construction of complex types
- Create sealed traits to restrict implementations to a closed set
- Design state machines where invalid transitions are compile-time errors, not runtime panics
How to install m05-type-driven
npx skills add https://github.com/actionbook/rust-skills --skill m05-type-drivenHow to use m05-type-driven
- 1.Identify where runtime validation or boolean flags are used in your code
- 2.Ask: can the compiler catch this error instead? Can invalid states be unrepresentable?
- 3.Choose the appropriate pattern: newtype for primitives, type state for state machines, marker traits for capabilities, builder for gradual construction
- 4.Implement validation at construction time (e.g., Email::new) and trust the type thereafter
- 5.Use PhantomData when you need to track type parameters without runtime overhead
Use cases
- Wrapping primitives with semantic meaning (e.g., UserId, Email) to prevent type confusion
- Building connection or session types that enforce valid state sequences (Disconnected → Connected → Authenticated)
- Preventing invalid configurations by making them unrepresentable in the type system
- Validating data once at construction and trusting it forever thereafter
- Implementing capability-based security where marker traits prove preconditions
- Rust developers designing APIs or domain models
- Teams building state machines or protocol implementations
- Anyone replacing runtime validation with compile-time guarantees
- Library authors enforcing invariants through types
m05-type-driven FAQ
Use newtype for wrapping primitives with semantic meaning (UserId, Email). Use type state when you need to encode state transitions and prevent invalid operation sequences.
PhantomData is a zero-sized type that tracks type parameters for variance and lifetime control. A marker trait is a capability flag that proves a type has been validated or has certain properties.
Create a newtype with a private constructor and a public `new()` method that validates. Once constructed, the type guarantees the invariant holds.
No. Use type-driven design for invariants the compiler can check (state, semantic meaning, ranges). Use Result for errors that depend on external input or runtime conditions.
A sealed trait restricts implementations to a closed set by making the trait private and only implementing it for specific types. Use it when you want to prevent external crates from implementing a trait.
Full instructions (SKILL.md)
Source of truth, from actionbook/rust-skills.
name: m05-type-driven description: "CRITICAL: Use for type-driven design. Triggers: type state, PhantomData, newtype, marker trait, builder pattern, make invalid states unrepresentable, compile-time validation, sealed trait, ZST, 类型状态, 新类型模式, 类型驱动设计" user-invocable: false
Type-Driven Design
Layer 1: Language Mechanics
Core Question
How can the type system prevent invalid states?
Before reaching for runtime checks:
- Can the compiler catch this error?
- Can invalid states be unrepresentable?
- Can the type encode the invariant?
Error → Design Question
| Pattern | Don't Just Say | Ask Instead |
|---|---|---|
| Primitive obsession | "It's just a string" | What does this value represent? |
| Boolean flags | "Add an is_valid flag" | Can states be types? |
| Optional everywhere | "Check for None" | Is absence really possible? |
| Validation at runtime | "Return Err if invalid" | Can we validate at construction? |
Thinking Prompt
Before adding runtime validation:
-
Can the type encode the constraint?
- Numeric range → bounded types or newtypes
- Valid states → type state pattern
- Semantic meaning → newtype
-
When is validation possible?
- At construction → validated newtype
- At state transition → type state
- Only at runtime → Result with clear error
-
Who needs to know the invariant?
- Compiler → type-level encoding
- API users → clear type signatures
- Runtime only → documentation
Trace Up ↑
When type design is unclear:
"Need to validate email format"
↑ Ask: Is this a domain value object?
↑ Check: m09-domain (Email as Value Object)
↑ Check: domain-* (validation requirements)
| Situation | Trace To | Question |
|---|---|---|
| What types to create | m09-domain | What's the domain model? |
| State machine design | m09-domain | What are valid transitions? |
| Marker trait usage | m04-zero-cost | Static or dynamic dispatch? |
Trace Down ↓
From design to implementation:
"Need type-safe wrapper for primitives"
↓ Newtype: struct UserId(u64);
"Need compile-time state validation"
↓ Type State: Connection<Connected>
"Need to track phantom type parameters"
↓ PhantomData: PhantomData<T>
"Need capability markers"
↓ Marker Trait: trait Validated {}
"Need gradual construction"
↓ Builder: Builder::new().field(x).build()
Quick Reference
| Pattern | Purpose | Example |
|---|---|---|
| Newtype | Type safety | struct UserId(u64); |
| Type State | State machine | Connection<Connected> |
| PhantomData | Variance/lifetime | PhantomData<&'a T> |
| Marker Trait | Capability flag | trait Validated {} |
| Builder | Gradual construction | Builder::new().name("x").build() |
| Sealed Trait | Prevent external impl | mod private { pub trait Sealed {} } |
Pattern Examples
Newtype
struct Email(String); // Not just any string
impl Email {
pub fn new(s: &str) -> Result<Self, ValidationError> {
// Validate once, trust forever
validate_email(s)?;
Ok(Self(s.to_string()))
}
}
Type State
struct Connection<State>(TcpStream, PhantomData<State>);
struct Disconnected;
struct Connected;
struct Authenticated;
impl Connection<Disconnected> {
fn connect(self) -> Connection<Connected> { ... }
}
impl Connection<Connected> {
fn authenticate(self) -> Connection<Authenticated> { ... }
}
Decision Guide
| Need | Pattern |
|---|---|
| Type safety for primitives | Newtype |
| Compile-time state validation | Type State |
| Lifetime/variance markers | PhantomData |
| Capability flags | Marker Trait |
| Gradual construction | Builder |
| Closed set of impls | Sealed Trait |
| Zero-sized type marker | ZST struct |
Anti-Patterns
| Anti-Pattern | Why Bad | Better |
|---|---|---|
| Boolean flags for states | Runtime errors | Type state |
| String for semantic types | No type safety | Newtype |
| Option for uninitialized | Unclear invariant | Builder |
| Public fields with invariants | Invariant violation | Private + validated new() |
Related Skills
| When | See |
|---|---|
| Domain modeling | m09-domain |
| Trait design | m04-zero-cost |
| Error handling in constructors | m06-error-handling |
| Anti-patterns | m15-anti-pattern |
Related skills
More from actionbook/rust-skills and the wider catalog.

m06-error-handling
Master Rust error handling: when to use Result, Option, panic, and how to propagate errors effectively.

m07-concurrency
Master Rust concurrency: threads, async/await, channels, and thread-safe primitives.

m09-domain
Model domain concepts as Entities, Value Objects, and Aggregates using Rust ownership and type safety.

m10-performance
Measure, identify bottlenecks, then optimize with data structures, allocation strategies, and parallelism.

m11-ecosystem
Navigate Rust crate selection, dependency management, and ecosystem integration decisions.

m12-lifecycle
Design resource creation, usage, and cleanup patterns using RAII, Drop, and lifecycle strategies.