cdd
Compiler Driven Development methodology for scala-ts — Scala 3 generator code and the TypeScript/io-ts it emits. Use when writing, editing, or refactoring code to maximize compile-time correctness gua
SKILL.md.claude/skills/cdd/SKILL.md
GitHubCompiler Driven Development (CDD)
Push correctness from runtime to compile time by encoding invariants in types. In this repo that applies twice: to the Scala that does the generating (TsParser, TsModel, TsGenerator), and to the TypeScript it emits, which its consumers type-check under strict. Scala toolkit: scala-type-safety. Step-by-step loop: compiler-driven-verification. Output shapes: scala-ts-generation, io-ts.
Enforcement order: types → lint (scalac -Werror with sbt-tpolecat's flags, -Ycheck-all-patmat) → tests (golden output + ts-node round-trip) → runtime checks only where data enters.
Construct map (Scala ↔ generated TS)
| Principle | Scala (this repo) | Generated TypeScript |
|---|---|---|
| Absent value | Option[A] (no null) |
O.Option<A> via optionFromNullable |
| Fallible result | Either[E, A]; generation-time failure is sys.error / report.errorAndAbort with the type name |
E.Either<L, R> via io-ts-types either |
| Closed set of cases | sealed trait / enum (TsModel) |
_tag union: t.union([...]), XU, XCU |
| Exhaustiveness | -Ycheck-all-patmat + -Werror on a match that lists every case |
consumers' switch on _tag narrows because every member carries a literal _tag |
| No unsafe casts | no new asInstanceOf/isInstanceOf |
output never needs as; codecs end in satisfies t.Type<X, unknown> |
| Immutability | immutable collections, no var |
ReadonlyArray, ReadonlySet, ReadonlyMap, ReadonlyNonEmptyArray, t.readonly |
| Boundary validation | the macro reads the type once, into TsModel |
io-ts decode at the consumer's edge |
Core principles
- Make illegal states unrepresentable — a sealed ADT over a record of flags; a new distinction in the generator is a new
TsModelcase or a private ADT, not aBooleanparameter. - Parse, don't validate —
TsParserturns a Scala type into aTsModelonce; the generator never re-inspects Scala types or re-parses strings it generated. - The compiler is the refactoring guide — change the type first, follow the errors; each fix must be semantically complete, never a silencer.
- Verify type-system claims with the compiler, not memory. For a claim about Scala, write a scratch test and compile it (
sbt Test/compile). For a claim about TypeScript or io-ts, put the code intests/output/scratch.tsand runcd tests && yarn --silent ts-node output/scratch.ts(strict type-check), then delete it. - Generated output must not be weaker than the Scala type. No
{},any, or mutable collections in emitted types; a field-less shape still gets a precise type.
Workflow
- Define or change the type (a
TsModelcase, a field, a private ADT). - Follow compiler errors —
sbt Test/compile. - Fix each error correctly — no
asInstanceOf, nocase _ =>over a sealed type, no@nowarn. - Pin the output — update or add the golden expectation in the matching
CodecTestsuite. - Verify per
verification-before-completion.
Anti-patterns
case _ =>overTsModel→ list every case (the compiler then flags new variants).asInstanceOfto reach a runtime value → capture a typed conversion function inTsParser(likeTsModel.Array.toList).- Silently emitting looser TypeScript for an unsupported shape → fail generation with the type name.
- A
Booleanmode parameter that callers can swap → a two-case ADT. varor mutable builders → fold /foldMap/|+|onGenerated.