Nim Distinct Types: Turning Unit Mix-Ups into Compiler Errors
Nim distinct types make passing meters where feet are expected a compile-time error. See a worked example, the boundary friction, and how to roll it out safely.
15 May 2026, 05:31 UTC

The interchangeable-number trap
Most Nim programs start with plain int and float for everything. That works until the day you pass an orderId into a proc that expects a userId, or a length in feet into a routine that assumes meters. The code compiles, the tests might even pass if the values happen to be small, and the failure surfaces later as corrupted data or a confusing runtime bug.
Nim's distinct types exist for exactly this situation. They create a new type that shares the same memory representation as its base type but refuses to mix with it implicitly. The mistake that used to be a runtime surprise becomes a compile-time error, and you pay nothing at runtime because the underlying value is unchanged.
What distinct actually does
Writing type UserId = distinct int declares a separate type. You cannot assign a plain int to a UserId without an explicit conversion. Values are constructed with UserId(7) or the shorter 7.UserId. Until you convert or borrow the base value, normal integer operators are unavailable. That friction is the point: every boundary crossing becomes visible in the source.
A worked example with units
Suppose you are calculating floor area. The formula is simple, but the unit matters. Here is a small program that defines Meters and Feet as distinct floats and an area proc that accepts only Meters.
type
Meters = distinct float
Feet = distinct float
proc area(side: Meters): float =
let s = float(side)
result = s * s
proc `$`(m: Meters): string =
result = $float(m) & "m"
let correct = area(Meters(3.0))
echo correct
# let wrong = area(Feet(3.0)) # This line is rejected by the compiler.
The call area(Feet(3.0)) is rejected because Feet and Meters are different types, even though both wrap a float. The call area(Meters(3.0)) compiles. Inside area, the explicit float(side) conversion unwraps the distinct value so you can multiply. Nim also offers a borrow mechanism that lets a routine temporarily treat a distinct value as its base type; a formatting proc can use it to reuse float operations without writing a manual cast each time. That mechanism is scoped to the routine where it appears, so it is not a general-purpose cast, and using it broadly can hide the boundaries you wanted to make explicit.
The boundary friction you will feel
Distinct types shift work from runtime debugging to compile-time plumbing. JSON libraries, database drivers, and generic math routines usually expect built-in types, so you will write small helpers to parse and serialize your distinct values. A large family of distinct types can also make proc signatures more verbose unless you centralize conversion logic in one module.
Two cautions deserve emphasis. First, avoid converter as a shortcut. A converter silently reintroduces implicit conversions and defeats the purpose of the distinct type. Second, distinct types prevent mixing but do not validate ranges. A UserId can still hold a nonsensical negative number unless you check it at parse time.
A practical rollout
Start with the identifiers and units that have already caused bugs. Add a small helper module for parsing and serialization, then let the compiler enumerate every call site that must be updated. The error list becomes your to-do list.
To verify the behavior on your machine, create a file named main.nim with the example above, uncomment the rejected line, and run:
nim c -r main.nim
Run this from the directory containing the file; no special permissions are required. The compiler should reject the mismatched call and accept the file once the line is commented out again. If you use a specific memory management mode, compile with an explicit flag such as nim c --mm:orc -r main.nim to confirm your local toolchain accepts the syntax. Check your compiler version first with nim --version, because details of distinct, borrow, and generic matching have shifted across Nim releases.
The result is a program where the type system carries a little more of the domain meaning. The cost is a handful of explicit conversions; the benefit is that a whole class of silent mix-ups never reaches production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.