Nominal and Opaque Types

This example uses nominal types (:=) to keep meters and seconds distinct, even though both contain a number. It then uses an opaque nominal type (::) to ensure that a Positive value can only be created through a constructor that rejects zero.


import Positive

Meters := { value : U64 }.{
	is_eq : _
}

Seconds := { value : U64 }.{
	is_eq : _
}

distance = Meters.{ value: 10 }

duration = Seconds.{ value: 10 }

expect distance.value == duration.value
# distance == duration would be a type error: Meters and Seconds differ.

expect {
	count = Positive.from_u64(3)?
	count.to_u64() == 3
}
expect Positive.from_u64(0) == Err(Zero)
# Positive.(0) is forbidden here: only Positive.roc can construct it.

The Positive type lives in a separate module, where its backing number is hidden from callers. Its constructor is the only way for this app to create a value, so the check against zero cannot be bypassed:

Positive :: U64.{
	is_eq : _

	from_u64 : U64 -> Try(Positive, [Zero])
	from_u64 = |n| if n == 0 {
		Err(Zero)
	} else {
		Ok(Positive.(n))
	}

	to_u64 : Positive -> U64
	to_u64 = |Positive.(n)| n
}

If we only wanted a shorter name for an existing type, we could use a type alias (:). Unlike the nominal types above, an alias does not give the type a separate identity.

Output

Run this from the directory that has main.roc in it:

$ roc test main.roc

All (3) tests passed in 10.2 ms.

Validated with Roc nightly-2026-10-29-a0a9fc2.