Skip to content

Instantly share code, notes, and snippets.

@jul-sh
Created January 9, 2026 18:43
Show Gist options
  • Select an option

  • Save jul-sh/bf418a751c5272ad442df7f68704767c to your computer and use it in GitHub Desktop.

Select an option

Save jul-sh/bf418a751c5272ad442df7f68704767c to your computer and use it in GitHub Desktop.
This skill is expanded to be language-agnostic, anchoring the concept in **Algebraic Data Types (Sum Types)**. This provides the theoretical basis for *why* this prevents bugs (math/cardinality) and provides concrete implementation patterns for modern languages like Rust, TypeScript, Python, and Kotlin.
---
---
## name: enum-driven-state description: Enforce type-safe state modeling using Sum Types (Enums, Discriminated Unions, Sealed Classes) to make invalid states unrepresentable. Refactor parallel booleans, nullable "conditional" fields, and loose flags into exclusive cases with associated data.
# Enum-Driven State (Make Invalid States Unrepresentable)
Use this skill to refactor code where the state is modeled loosely (products of optional types) into strict models (sums of specific types). The goal is to rely on the type system to enforce valid logic, ensuring that a specific state cannot exist without its required data, and that mutually exclusive states cannot coexist.
## Core Principles
1. **Make Illegal States Unrepresentable**: Instead of relying on runtime validation or comments to ensure data integrity, structure the types so that invalid combinations (e.g., `isLoading = true` AND `errorMessage != null`) simply cannot compile.
2. **Sum Types over Product Types**:
* *Anti-pattern (Product):* A struct with `status`, `data?`, and `error?`. The cardinality is . Most combinations are invalid.
* *Pattern (Sum):* An enum/union where `Success` holds `Data` and `Failure` holds `Error`. The cardinality is (only valid combinations).
3. **Pattern Matching**: Replace imperative boolean checks (`isX`) with exhaustive pattern matching (`switch`, `match`, or discriminated checks) to ensure new states force compiler errors at call sites.
4. **Parse, Don't Validate**: Push checks to the boundary. Once an object exists, it should be in a guaranteed valid state.
5. **Co-location of Data**: Data that is only valid in a specific state must reside *inside* the definition of that state, not as a sibling field.
## Heuristics for Detection
* **The "Bag of Optionals"**: A class/struct where 50%+ of fields are optional/nullable, but comments say "Required if type is X".
* **Parallel Booleans**: `isLoading`, `isSuccess`, `hasError` defined as separate flags on the same object (The "Boolean Explosion").
* **Computed Flags**: Properties like `get isVideo() { return this.type == 'video' }` used to gate access to other fields.
* **Temporal Coupling**: Variables that are initialized as null/empty and only populated after a specific function call (e.g., `connect()` must be called before `socket` is valid).
* **Defensive Coding**: Code littered with `if (data != null)` inside logic that already checked `if (isSuccess)`.
## Implementation Guide by Language Family
Since not all languages have "Enums with associated values," apply the concept as follows:
* **Rust / Swift**: Use `enum` with associated values (Tuple variants or Struct variants).
* **TypeScript**: Use **Discriminated Unions**. A union of object literals that all share a common literal `type` or `kind` property.
* **Python (3.10+)**: Use `dataclasses` combined with `typing.Union` (`|`) and structural pattern matching (`match`/`case`).
* **Kotlin**: Use `sealed interface` or `sealed class` hierarchies where specific states are subclasses.
* **Java (17+)**: Use `sealed interface` with `record` implementations.
* **Go**: Use an interface defining the closed set of behaviors, or a struct with a discriminator field and pointers to child structs (where only one is non-nil).
## Refactor Checklist
1. **Identify Mutually Exclusive States**: Map out which states cannot exist simultaneously.
2. **Group Data**: Move fields that belong to a specific state into a dedicated struct/tuple/case for that state.
3. **Create the Sum Type**: Define the Enum or Union that wraps these groups.
4. **Update Call Sites**: Replace property access (`obj.optionalField`) with pattern matching (`case .success(let data): ...`).
5. **Remove Invariants**: Delete the validation logic that was previously required to ensure state consistency (e.g., `assert(data != null if !loading)`).
## Examples
### TypeScript: Discriminated Unions (Async Data)
**Before (Invalid state possible: `loading: false`, `data: undefined`, `error: undefined`):**
```typescript
interface State<T> {
status: 'idle' | 'loading' | 'success' | 'error';
data?: T; // ambiguous: is data present if status is error?
error?: Error;
}
```
**After (Invalid state unrepresentable):**
```typescript
type State<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T } // data is required here
| { status: 'error'; error: Error }; // error is required here
// Usage
function render(state: State<User>) {
// TypeScript narrows the type automatically based on the check
if (state.status === 'success') {
console.log(state.data.name); // Safe
}
// console.log(state.data); // Error: Property 'data' does not exist on 'idle'
}
```
### Rust: Enums with Data (Connection State)
**Before (Struct with optionals):**
```rust
struct ConnectionConfig {
protocol: String, // "http" or "ssh"
url: String,
port: Option<u16>, // Meaningless for some protocols
ssh_key: Option<String>, // Only for SSH
}
```
**After (Enum with variants):**
```rust
enum Connection {
Http { url: String, port: u16 },
Ssh { host: String, key: String },
Local { path: String }
}
// Usage
match connection {
Connection::Http { url, port } => connect_http(url, port),
Connection::Ssh { host, key } => connect_ssh(host, key),
Connection::Local { path } => connect_pipe(path),
}
```
### Python: Union of Dataclasses (Business Logic)
**Before (Ambiguous Class):**
```python
@dataclass
class PaymentMethod:
type: str # "cc", "paypal"
cc_number: Optional[str] = None
paypal_email: Optional[str] = None
```
**After (Union of Dataclasses with Pattern Matching):**
```python
from dataclasses import dataclass
@dataclass
class CreditCard:
number: str
cvv: str
@dataclass
class PayPal:
email: str
# The type is strictly one OR the other
PaymentMethod = CreditCard | PayPal
def process(payment: PaymentMethod):
match payment:
case CreditCard(number=n, cvv=c):
charge_card(n, c)
case PayPal(email=e):
send_invoice(e)
```
### Kotlin: Sealed Classes (UI State)
**Before:**
```kotlin
data class UiState(
val isLoading: Boolean = false,
val items: List<Item>? = null,
val error: String? = null
)
```
**After:**
```kotlin
sealed interface UiState {
data object Loading : UiState
data class Content(val items: List<Item>) : UiState
data class Error(val message: String) : UiState
}
// Usage (Exhaustive `when` required)
when (state) {
is UiState.Loading -> showSpinner()
is UiState.Content -> showItems(state.items) // Smart cast to Content
is UiState.Error -> showToast(state.message)
}
```
## When to Stop (Boundaries)
* **Database Models**: Relational DBs are inherently flat. It is acceptable to have a flat DTO (Data Transfer Object) that maps to columns, but immediately convert it to the Enum-Driven State when it enters the domain logic layer.
* **Cross-cutting Concerns**: If an `id` or `timestamp` exists on *all* states, keep it on the top-level container rather than duplicating it inside every enum variant (e.g., `struct Event { id: String, content: EventContentEnum }`).
* **Over-nesting**: If an Enum case requires 10 different unrelated fields, define a dedicated struct for that case rather than stuffing arguments into the Enum constructor.
* **Serialization**: Standard JSON is untyped. Ensure your serialization library (e.g., `Serde` in Rust, `Zod` in TS, `Moshi` in Kotlin) is configured to handle "Polymorphic" types or "Tagged Unions" correctly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment