Stop Mixing Meters and Seconds: Nim Distinct Types as Zero-Cost Guards
Mixing meters and seconds compiles in most languages. Nim's distinct types catch that at compile time with no runtime overhead, forcing explicit conversions for semantically different values that share a representation.
15 Aug 2025, 12:25 UTC

Adding a userId to an orderId compiles in most languages. The values are both ints, the code runs, and the bug hides until production. The same class of silent mistake happens with physical units: meters and seconds share a float representation, so a function that expects distance can quietly receive time.
Nim's distinct types give zero-cost compile-time separation for semantically different values with identical representation. They are not a runtime wrapper and they do not change memory layout, but the compiler treats Meters and Seconds as incompatible unless you explicitly convert.
What distinct is, and what it is not
A type alias in Nim is just another name for the same type. type Meters = float lets the compiler freely substitute float where Meters is expected. An enum gives a closed set of named values, which is not the intent for measurements.
A distinct type is a new type whose underlying representation is the base type, but with a distinct identity. type Meters = distinct float creates a type that occupies the same size as float, but the compiler will not implicitly allow a float or a Seconds to flow into a Meters.
This is compile-time only. No extra field, no runtime tag is added. The safety comes from the type checker, not from a runtime check.
Compile-time enforcement and explicit conversion
Because distinct types are incompatible, operations that mix them are rejected at compile time. That rejection is the useful signal: the code must name the conversion it intends.
Operators are not automatically available. Addition, comparison and arithmetic are defined for the base type, but not for the distinct wrapper. If you write a proc that adds two Meters, you must first define the operator for Meters, or convert to the base type explicitly.
Generics interact with distinct types in a predictable way. A generic proc proc add[T](a, b: T): T will instantiate separately for Meters and for Seconds. It will not allow mixing the two, because the type parameter is fixed per call site.
Worked example: Meters and Seconds
Define two distinct types over float:
type
Meters = distinct float
Seconds = distinct float
proc toMeters(s: Seconds): Meters =
Meters(float(s))
With these definitions, a call that adds a Meters value to a Seconds value is a type mismatch. The compiler rejects it because the operand types differ, even though both are distinct float.
When the conversion is intentional, you write it explicitly:
let m: Meters = Meters(10.0)
let s: Seconds = Seconds(2.0)
let m2 = m + Meters(5.0)
let mFromS = toMeters(s)
The conversion proc toMeters documents intent and provides a single place to enforce any domain rule, such as unit validation.
To verify zero-cost, compile a small file with nim c --out:prog test.nim run from a writable directory, and inspect the generated C for a distinct variable. The declaration should map to the base type with no extra wrapper or field. That inspection confirms the representation is unchanged.
Trade-offs and limitations
Distinct requires explicit conversion for every operation you want to allow. That verbosity is the safety mechanism, but it can feel heavy in generic numeric code where you would like implicit promotion.
Operator overloading must be repeated per distinct type. If you need + for Meters, you define it for Meters. You cannot share it automatically with Seconds.
Safety can be bypassed. Unsafe casts and low-level interop can force a distinct value into its base representation, removing the compile-time guard. Distinct types are a discipline, not a sandbox.
Heavy use with many overloads can increase compile time and code size due to duplicated instantiations.
Actionable use
Audit a codebase for easily confused scalars: IDs, money, measurements, timestamps. Apply distinct to the pairs that have caused bugs. Pair each distinct type with a small conversion API that makes intent explicit. Treat distinct as a design signal for where invariants matter, not as a blanket wrapper for every value.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.