scala-type-safety
The Scala 3 type-safety toolkit and compiler-flag rules for scala-ts — choosing a type-level tool, exhaustiveness, the @nowarn policy, macro-specific rules, and what the compiler does not check here.
SKILL.md.claude/skills/scala-type-safety/SKILL.md
GitHubScala type safety (CDD toolkit)
Every idiom here exists to move a class of bug from runtime (or review) to the compiler. When a change is hard to express with these, the model is wrong — change the types first, then follow the errors (cdd, compiler-driven-verification).
Compiler flags are the lint layer
sbt-tpolecat (in its default CI mode) sets -Werror, -Ycheck-all-patmat, -Wvalue-discard, -Wnonunit-statement and -Wunused:{implicits,explicits,imports,locals,params,patvars,privates}; see them with sbt 'show scalacOptions'. There is no scalafmt or scalafix. Every warning is a build failure — fix the code, never suppress it.
Choosing a type-level tool
| Need | Tool | What it rules out |
|---|---|---|
| A closed set of cases | sealed trait / enum (TsModel, TsImport, TsImport.Location) |
Stringly-typed states; missing cases in matches |
| A mode switch | a two-case ADT | A swapped Boolean argument that compiles silently |
| Two values of one base type must not mix | opaque type with its own companion |
Passing one kind of name where another goes |
| Construction must be validated | opaque type / private constructor + smart constructor returning Option/Either |
Unvalidated values existing at all |
| A literal must be valid | inline + compiletime.error, or report.errorAndAbort in a macro |
Bad input reaching generation |
| A type-level parameter without a value | a proxy type param (TypeParam["A1"]) |
Inference drifting |
| An erased runtime value must be read | capture a typed conversion at parse time (TsModel.Array.toList: Any => List[Any]) |
Casts scattered through the generator |
Exhaustiveness
-Ycheck-all-patmat+-Werrormake a non-exhaustive match on a sealed type a build error — but only if the match lists cases. A wildcardcase _ =>is not checked and swallows new variants. List every case; a wildcard is acceptable only when the scrutinee is genuinely open (strings, ints, arbitraryAnyvalues).- A union of case types (
TsModel.Object | TsModel.Interface) is matched exhaustively the same way.
@nowarn policy
- Never add
@nowarnto silence a real warning. - The one standing use is
@annotation.nowarn("msg=match may not be exhaustive")on quote-pattern type matches (tpe.asType match { case '[t] => ... }) inside the macro, which are exhaustive but cannot be proven so. - A parameter that exists only for its type (evidence or a proxy) is unused by definition; prefer a memberless marker type (exempt from the warning) over
@nowarn.
Macro rules (TsParser, ReflectionUtils)
- Abort with
report.errorAndAbortand a message naming the type when a shape is unsupported at compile time; usesys.erroronly for failures that can only be detected when generating. - Keep the macro producing data (
Expr[TsModel]), never TypeScript text — rendering belongs toTsGenerator. - Top-level vs reference parsing (
top) decides whether a definition is produced; parse referenced types withtop = falseso each type is defined once.
Instances: least power, no ambient defaults
- Make the compiler force a decision. Don't let a default parameter or "no instance in scope" mean "do nothing".
- Take a
usingparameter only when there is more than one meaningful instance (TsCustomType,TsCustomOrd,TsImports.Availableare the consumer's choices).
Things the compiler does not check here
- Golden output. Only the tests pin the generated text; a renderer change compiles regardless of what it emits.
- Generated TS type-correctness. Only the ts-node round-trip checks it, and only for files the suite's
decode-encode.tsimports. - Scala JSON vs generated codecs. The round-trip tests use circe with the
_tagdiscriminator; a consumer with different encoders is not checked. - Name-string matching.
TsCustomTypematches fully-qualified names as strings. - Run-time failures in generation — unresolved imports, missing config, missing
Ord(scala-ts-generation→ Footguns). - Wildcard matches,
sys.error, andasInstanceOfonAnyvalues are invisible to the compiler — review rejects new ones.
Preferred vs legacy (don't copy the legacy)
| Prefer | Legacy still present |
|---|---|
given/using, extension |
implicit def liftString and implicit B: Monoid[B] in TsGenerator |
| explicit cases over a sealed type | case _ => in recordCodecType/recordValueType/recordCodecInstance/generateTopLevel |
a typed conversion captured in TsParser |
asInstanceOf on values inside generateValueInstance |