$npx -y skills add lucasfcosta/backpressured --skill type-design-reviewUse when a backpressured code-review subagent is judging whether a diff uses the type system to make illegal states unrepresentable — i.e. the diff touches data models, function signatures, or domain types. TypeScript-centric, with mappings to Rust, Swift, Kotlin, and Haskell/OCa
| 1 | # Type-Design Review |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | **Push every invariant you can into the types, so the compiler — not a runtime check, not a test, not a human reviewer — is the thing that says "no."** This is the type-system instance of backpressure: a contradictory state that cannot be *constructed* needs no defensive check, no test, and no "this should never happen" comment, because it genuinely never can. |
| 6 | |
| 7 | You are reviewing a diff for this property. Your job is not "do the types compile" — it is "could a wrong state have been represented at all, and if so, why wasn't it designed out?" |
| 8 | |
| 9 | Three ideas, one throughline: |
| 10 | - **Make impossible states impossible** (Richard Feldman, elm-conf 2016): if the app forbids a combination of data, design the data model so that combination cannot be expressed. |
| 11 | - **Parse, don't validate** (Alexis King, 2019): don't check data and throw the knowledge away — parse it into a type whose existence *is the proof* of the invariant, then carry that type inward. |
| 12 | - **Make illegal states unrepresentable** (Yaron Minsky, *Effective ML*, c. 2010): making the wrong thing hard to express beats checking for the wrong thing at runtime. |
| 13 | |
| 14 | ## When to Use |
| 15 | |
| 16 | - A `backpressured` code-review subagent is reviewing a diff (per-iteration or whole-changeset) and the diff introduces or changes **data models, function signatures, or domain types**. |
| 17 | - New state is modelled with **booleans / optional fields**, new `switch`/`match` over a union, new `as`/`!` assertions, or new functions taking raw `string`/`number`. |
| 18 | |
| 19 | **Not for:** pure formatting, dependency bumps, or changes with no type-level surface. **And not as a general reviewer** — correctness/logic bugs, simplicity/reuse, and test quality belong to the general code-review reviewer. This skill is *only* the type-design lens: could a wrong state have been represented at all? |
| 20 | |
| 21 | ## The one question — apply it to every type in the diff |
| 22 | |
| 23 | For each data model, signature, or domain type the diff adds or changes: **enumerate the states the type permits, then ask which of those the domain forbids.** Each permitted-but-forbidden state is a candidate finding, and the fix is to restructure so it can no longer be expressed. That single question *is* the review — the smells below are common instances of it, **not a closed checklist**. A diff can make an illegal state representable in a shape no row names (`end` before `start`, `min > max`, a list that should be a set, three different `null`s meaning three different things); that still counts, and you are expected to catch it from the principle. Run the question first; reach for the table for vocabulary and ready-made fixes, not as the boundary of what to look for. |
| 24 | |
| 25 | ## The smells — common instances (not a checklist) |
| 26 | |
| 27 | Treat each row as a finding **only when it actually makes a domain-forbidden state representable here** — not on sight. The left column is a pattern to *notice*; it becomes a finding only once you can name the real illegal state it permits in this code (a matching pattern that forbids nothing real is not a finding — see *Proportionality*). |
| 28 | |
| 29 | | Smell | Why it's a bug waiting to happen | Push toward | |
| 30 | |-------|----------------------------------|-------------| |
| 31 | | Boolean flags that can't all be true at once (`isLoading` + `isError` + `isSuccess`) | `2^n` combinations exist; most are nonsense the types permit | one discriminated union with a `status` discriminant | |
| 32 | | Optional fields that are really mutually exclusive (`error?` **and** `data?`) | "loaded *and* erroring" is representable but meaningless | a tagged union, one variant per real state | |
| 33 | | `as` / `as any` / `<T>x` casting into or out of a union | an assertion the compiler can't verify — reintroduces the runtime error types exist to prevent | narrow with a user-defined type guard, or parse | |
| 34 | | Non-null assertion `!` to silence "possibly undefined" | crashes at runtime if the value *is* null | narrow with a check, or restructure so it can't be null | |
| 35 | | `switch`/`match`/`when` with no exhaustiveness check | adding a variant later silently falls through instead of failing the build | `default: return assertNever(x)` (see below) | |
| 36 | | Primitive obsession / stringly-typed (`string` for ids, status, email, money, units) | `UserId` and `OrderId` are interchangeable; typos compile | branded/newtype/opaque types, literal unions | |
| 37 | | Validation returning `boolean` or `void` | the knowledge is discarded; every caller must re-trust | a parser returning a proof-carrying type | |
| 38 | | Two fields that must stay in sync (`list` + `selectedIndex`, cached `count` + array) | desync is representable; one can be updated without the other | a single structure that makes d |