Created
January 9, 2026 18:43
-
-
Save jul-sh/bf418a751c5272ad442df7f68704767c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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