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

  1. 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.
  2. 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.
  3. 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.
  4. 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

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.

Emacs 29.3 (Org mode 9.6.15)