Compass Principle Type System Discipline
Table of Contents
1. When to use this skill
Cite it with principle-type-system-discipline whenever you design a type
or a signature here. The upstream rule is written for TypeScript. Its
intent carries to C++ unchanged; several of its mechanisms do not, and two
of its patterns are harder here than the original implies.
Read the upstream skill for the rule. Read this for what it means in this codebase.
2. How to use this skill
- Take the intent from upstream. Illegal states unrepresentable, semantic primitives branded, external data parsed at the boundary, no lying to the compiler, matches exhaustive, types derived from the authoritative schema, strengthened only where partiality appears.
- Translate the mechanism using the table below. A pattern whose TypeScript form has no C++ equivalent still has a C++ answer; it is usually a different construct rather than an absent one.
- Watch the two the language does not give you. Exhaustive matching and branding are both weaker here than upstream assumes, and both are worth the extra ceremony.
- Prefer the generator. Where the shape is owned by an entity model, deriving the type means regenerating it, not hand-writing a parallel struct. This is the strongest form of "derive from the authoritative schema" available to us, and it is enforced.
2.1. The translation
| Upstream pattern | Here |
|---|---|
| Discriminated union | std::variant over per-state structs, or an enum class plus the payload the state carries. Not a struct of optionals with a comment saying which combinations are legal |
| Branded primitive | A one-field struct with explicit construction. A type alias brands nothing: it is the same type to the compiler, so using currency_code = std::string buys documentation and no safety |
| Parse at the boundary | The JSON and table I/O layers are the boundary. A domain type is constructed from parsed input, never handed a raw row |
| Do not lie to the compiler | const_cast, reinterpret_cast and a C-style cast are the hazards. static_cast between numeric types is the quiet one: it compiles, truncates, and says nothing |
| Exhaustive matching | The compiler will not tell you. See below |
| Derive from the schema | Regenerate from the entity model. A hand-written type that duplicates a generated shape is drift with a delay on it |
| Strengthen only where partiality appears | Same rule, and the same stopping point: if nothing would otherwise throw, leave the plain type |
2.2. Exhaustive matching, which the compiler now enforces
Upstream can rely on the compiler failing when a variant is added. We cannot, and the gap is wider than the language alone makes it.
-Wswitch fires on a switch over an enum class that misses an
enumerator, provided there is no default: label, and the build enables
it through -Wall with -Werror behind it. A missing case is a build
failure.
The condition is the whole rule: a default: on a closed enum turns
that error back into silence, and it is what an agent reaches for to
quiet a warning. Handle every enumerator explicitly instead. Roughly 174
files still carry a default: over a switch, so the backstop is real
for new code and absent wherever one of those labels sits.
For std::variant, an overload set with no catch-all fails to compile
on its own and needs no flag.
2.3. Branding, which costs more here
A branded type in C++ is a struct, which means writing the comparison
operators, the hash, the stream operator and the serialisation the
underlying primitive had for free. That cost is real and it is why the
codebase has bare std::string where a brand belongs.
The test is whether two arguments of the same primitive type mean different things and could be swapped at a call site without the compiler noticing. Where they can, the brand pays for itself the first time someone transposes them; where a type appears in one signature and never travels, it does not.
3. Recipes
- How do I build the system? — the warnings are only useful if the build is clean.
4. Reference
- Principles — the citation function, and why this binding exists.
- The pstack Collection — the upstream rule, referenced rather than copied.
- Data-oriented design — the other axis on a type decision here: shape for the machine as well as for the compiler.