Instead of re-validating in every mutator, some languages let you make the invalid state impossible to construct: Rust's NonZeroU32, Ada's `subtype Percent is Integer range 0 .. 100`, an OCaml abstract type with a smart constructor, or a TypeScript branded type. Compare where each of those actually enforces the constraint, and what each one costs.
answer
- make illegal states unconstructible, check once at the edge
- Rust NonZero: niche optimisation, Option same size
- new_unchecked turns a bug into UB
- Ada range subtype: re-checked every assignment
- TS brand / Python NewType erased - still validate at I/O
basics
~20 sPush the check to the boundary and let the type carry the proof afterwards. Rust's NonZeroU32 checks once at construction and never again; Ada re-checks its range on every assignment; an OCaml abstract type makes the smart constructor the only door; TypeScript's brand is erased at compile time and enforces nothing at runtime.
solid answer
~1 minFour different places to put the same guarantee. - **Rust `NonZeroU32` / `NonNull`**: constructed only through a checked `new` returning `Option`, then never re-checked — and the compiler exploits the forbidden value as a niche, so `Option<NonZeroU32>` is the same size as `u32`. The invariant becomes a layout fact. It pays with `new_unchecked`, whose misuse is undefined behaviour, so the invariant is now a soundness obligation rather than merely a bug. - **Ada `range` subtypes and `Dynamic_Predicate`**: re-checked on every assignment and parameter pass. You pay per store, but there is no cast that smuggles a bad value in, which is the point in the domains Ada serves. - **OCaml / SML / F#**: an abstract type in the signature plus `val create : int -> t option` means the constructor is the *only* path; the type system, not a runtime check, proves no other exists. Cost: every construction site handles failure, and clients cannot pattern-match the value. - **TypeScript brands and Python's NewType**: erased. Anything crossing an I/O boundary — `JSON.parse`, a request body — must still be validated by real code; the brand documents an invariant it cannot enforce, unlike JavaScript's `#`-field brand checks, which are real at runtime.
code
rust · 8 linesuse std::num::NonZeroU32;
let ok = NonZeroU32::new(7); // Some(..)
let no = NonZeroU32::new(0); // None -- the only safe door
assert_eq!(size_of::<Option<NonZeroU32>>(), size_of::<u32>()); // niche used for None
// unsafe { NonZeroU32::new_unchecked(0) } // not a wrong answer: undefined behaviourgo deeper
Understand the idea: validate once when a value is created, then rely on the type rather than re-checking in every method.
Name the enforcement point for at least two languages — construction-time in Rust, per-assignment in Ada — and know that TypeScript brands are erased.
Place the boundary deliberately: parse untrusted input into a validated type at the edge, and identify the bypass paths (deserialisers, ORMs, unchecked constructors) that would defeat it.
Weigh the failure cost against the enforcement cost, decide whether a violated invariant is a wrong answer or a soundness break, and plan for staged construction with an explicit draft type instead of half-legal domain objects.
## The move: from repeated checks to one proof Routing all mutation through invariant-preserving methods still leaves the predicate as a runtime obligation you re-discharge at each entry point. The stronger move is to make the illegal state unconstructible, so the check happens once at the point where an untrusted value enters and every function downstream expresses its requirement in its signature rather than in a guard. The languages differ sharply in *where the enforcement lives* — construction, assignment, the type system, or nowhere — and each location has a distinct cost. ## Rust: check once, and the compiler cashes the invariant in `NonZeroU32` wraps a `u32` that is guaranteed non-zero. The only safe way in is `NonZeroU32::new(x)`, which returns `Option<NonZeroU32>`; once you hold the value, no further check exists anywhere. Two consequences are worth naming. First, the invariant becomes a *representation* fact: because zero is now an impossible bit pattern, the compiler uses it as a niche for the `None` case, making `Option<NonZeroU32>` exactly as large as a `u32`. `NonNull<T>` does the same for pointers, which is why `Option<Box<T>>` is pointer-sized. An invariant expressed in the type has bought a layout optimisation — something a runtime guard can never do. Second, the escape hatch changes the *category* of the bug. `NonZeroU32::new_unchecked` is `unsafe`: violating the invariant there is not a wrong answer, it is undefined behaviour, because other code has already been compiled on the assumption. Encoding an invariant into a type makes it stronger and makes its violation worse. The same technique generalises with the newtype pattern: a struct with a private field plus a validating constructor. The type system guarantees no other construction path exists because a private field cannot be set from outside the module. ## Ada: enforcement at every assignment `subtype Percent is Integer range 0 .. 100` and `Dynamic_Predicate` place the check on every assignment, parameter pass and return. This is the opposite trade from Rust: you pay repeatedly, and in exchange the constraint holds no matter how the value was produced — there is no unchecked constructor to misuse. In the high-assurance domains Ada targets, that uniformity is the product. It also composes with Ada's `Type_Invariant` on private types, which is checked at the package boundary instead, so one language offers per-store and per-boundary granularity side by side and asks the designer to choose. ## ML-family: the constructor is the only door, by typing In OCaml, a `.mli` may declare `type email` abstractly and export `val of_string : string -> email option`. Clients cannot build an `email` any other way, cannot destructure one, and cannot forge one with a cast. There is no runtime mechanism at all after construction — the guarantee is that *no code exists* that could produce an invalid value, which is checked statically. F# expresses the same with a single-case union whose constructor is private; Haskell with a module that exports the type but not its data constructor, plus `NonEmpty` for the commonest structural case. The cost is real and should be stated: every construction site must handle the failure case, so the option or result threads through the parsing layer; and clients lose pattern matching, so the module must export enough accessors to be usable, which is a design burden on the API author. ## TypeScript and Python: documentation wearing a type's clothes A TypeScript branded type is `type Email = string & { readonly __brand: unique symbol }`, produced by a function that validates and casts. Inside the type checker it behaves like a distinct type. At runtime it is a string, because the brand is erased by the compiler — there is no field, nothing to check, and `x as Email` bypasses the constructor with no runtime cost or consequence. Python's `NewType` is the same story with the type checker outside the runtime entirely. That is not useless — it stops honest mistakes and documents intent — but it dictates the placement rule: **anything crossing an I/O boundary must still be validated by executing code**. A value from `JSON.parse`, a request body, a row from a database written by another service, arrives as `any` and can be asserted into a branded type without ever passing the validator. Contrast JavaScript's `#`-field brand check, which is a genuine runtime test precisely because the field really exists. ## Choosing Ask two questions. *Where does untrusted data enter?* — put the single check there, and make the value's type change as it crosses, so no downstream function has to trust or re-check. *What does a violation cost?* — if it is a wrong answer, a validated newtype or an abstract type is enough; if it corrupts memory or unlocks a resource, prefer the language that re-checks on every store, or forbid the unchecked constructor by policy. And know what you give up. Types that carry proofs make some legitimate operations awkward: a builder that must pass through intermediate invalid states, deserialisation frameworks that construct by reflection and bypass your constructor entirely, and ORMs that materialise objects field by field. Those bypass paths are where invariants encoded in types most often leak, and they are worth naming before someone finds them in production.
- Deserialisation frameworks often construct objects field by field, bypassing your validating constructor. How do you keep the invariant?Treat deserialisation as an untrusted boundary rather than as object construction: parse into a plain data shape first, then run the validating constructor to produce the real domain type, so there is exactly one door. Where the framework insists on building the domain type directly, configure it to use the constructor rather than reflection or field injection, and add a post-construct validation hook that fails the deserialisation rather than yielding a half-legal object. Reflection-built objects are the single commonest leak in invariants that were supposed to be guaranteed by construction.
- What legitimate designs become harder when a type refuses to represent intermediate states?Anything with a genuinely staged build: a form partially filled by a user, a message assembled across several network frames, a builder that accumulates fields before it can know whether the whole is valid. The standard answer is to have two types — an unvalidated draft type and a validated domain type — with one total function converting between them, which keeps the invariant intact and makes the staging explicit rather than smuggling half-built objects through the same type.
A wristband at a festival versus showing your ticket at every stage. The wristband is checked once at the gate and trusted everywhere inside (Rust); some venues re-check at every door (Ada); a wristband drawn on with a pen is a brand nobody can actually verify (TypeScript).
saying these in an interview costs you the question
- Believing a TypeScript branded type or a Python NewType prevents anything at runtime, and skipping validation of parsed JSON.
- Reaching for Rust's `new_unchecked` to avoid an Option, without recognising that a violation there is undefined behaviour rather than a wrong value.
- Assuming a private field plus a validating constructor is airtight when a reflection-based deserialiser can build the object field by field.
- Claiming a per-assignment range check like Ada's is strictly better, ignoring that it costs on every store and cannot express multi-field consistency.
- Encoding an invariant in a type but then exporting a setter that can break it, which puts you back to per-mutator checking with extra ceremony.