Types & Values
Primitive Types
Every value has a type known at compile time. Start with the everyday types, then refine only where an interface demands an exact width or an unsigned bit pattern.
Integers
int is the recommended default: it matches the machine word size and cannot overflow silently in checked builds. Fixed-width and unsigned types exist for binary formats, hashing and hardware registers.
let small = 42 # int — the default, pointer-sized integer
let limit = 255'i64 # explicit 64-bit width via a literal suffix
let bits = 0b1010'u8 # unsigned 8-bit: 10
let big = 9_000_000_000 # underscores group digits; value is still an int
echo small, " ", limit, " ", bits, " ", big
# Fixed widths: int8 int16 int32 int64, and their unsigned twins uint8..uint64.
# Unsigned arithmetic wraps; signed overflow raises OverflowDefect while checks
# are enabled (they are on for 'nim c' and '-d:release', off for '-d:danger').
let byte: uint8 = 200'u8
echo byte + 100'u8 # 44 — unsigned wrap-around is defined, not an error
Floats, Booleans and Characters
let ratio: float = 0.5 # float64 by default
let fast = 1.5'f32 # float32 when a layout or a C API requires it
echo ratio / 3.0 # 0.1666666666666667
let ready = true # bool: 'true' or 'false', never 0/1 by accident
let mark = 'A' # char: one byte, single quotes
echo ready, " ", mark # type is inferred from the literal, no annotation
# Conversions between numeric types are explicit, so nothing silently truncates:
let whole = int(ratio) # 0 — fractional part is discarded, not rounded
echo whole
Inference and Declarations
You rarely write a type twice. Use let for values fixed after initialization, var for values that change, and const for compile-time constants.
let, var, const
let host = "localhost" # immutable binding, type inferred as string
var hits = 0 # mutable binding
host = "example.org" # compile error: cannot assign to 'let' — caught early
const MaxRetries = 5 # compile-time constant; usable in array sizes
var counters: array[MaxRetries, int] # the constant sizes the array type
counters[0] = hits
echo counters.len, " ", counters[0]
# Declare first, assign later — the type must then be written explicitly.
var buffer: string
buffer = "allocated on demand"
echo buffer
Literals Adapt to Context
An integer literal is not forced to int; it takes the type the surrounding expression expects. This removes most casts you would need in stricter languages.
proc scale(value: float32): float32 = value * 2.0'f32
echo scale(2.5) # 5.0 — the literal is accepted as float32
let small: int8 = 100 # fits; nothing else has to be written
echo small, " ", small.int # explicit conversion when you need a wider type
Types That Encode Constraints
Two small type forms prevent entire classes of mistakes: ranges restrict a value to an interval, and distinct types make two identical representations incompatible on purpose.
Range Types
A range type is still an integer at runtime, but the compiler and the runtime checks reject values outside the interval. The classic use is an index or a percentage.
type Percent = range[0 .. 100]
let usage: Percent = 60 # accepted
echo usage, "%"
# let broken: Percent = 120 # compile error: 120 is outside the range
proc apply(p: Percent): int = p * 2
echo apply(50)
# Ranges are checked at runtime as well, which matters for computed values.
proc asPercent(n: int): Percent =
if n < 0: 0 else: min(n, 100).Percent # clamp before converting, never crash
echo asPercent(150), " ", asPercent(-5)
Distinct Types
distinct creates a new type with the same representation but no implicit compatibility. It is how libraries stop you from passing a meter where a second is expected.
type
Meters = distinct float
Seconds = distinct float
proc speed(distance: Meters, time: Seconds): float =
float(distance) / float(time) # explicit unwrapping inside the module
let track = Meters(400.0)
let lap = Seconds(50.0)
echo speed(track, lap), " m/s" # 8.0
# echo speed(lap, track) # compile error: arguments are not interchangeable
Conversions, Casts and Checks
Nim keeps a hard line between a conversion the compiler can verify and a cast that reinterprets bytes. Prefer conversions; reach for cast only when you are decoding a binary layout you already understand.
Checked Conversion
let text = "42"
let n = parseInt(text) # std/strutils: parses text into an int
echo n + 1 # 43
let d = 3.99
echo int(d) # 3 — conversion truncates toward zero, no rounding
echo round(d), " ", ceil(d) # 4.0 4.0 — std/math gives explicit rounding
echo $d # "3.99" — '$' is the stringify operator
Casts and Runtime Checks
# cast reinterprets the bits: fast, unchecked, and only safe on a known layout.
let raw = cast[uint32](42'i32) # the bit pattern of 42 seen as unsigned
echo raw
# Checks are compiler switches, not library calls:
# nim c -> checks on (default)
# nim c -d:release -> checks on, optimized
# nim c -d:danger -> checks removed: fastest, but a defect becomes UB
# Keep checks on until a profile tells you which check costs time.
| Switch | Range and overflow checks | Intended use |
|---|---|---|
nim c | Enabled | Everyday development |
nim c -d:release | Enabled | Benchmarks and releases |
nim c -d:danger | Disabled | Only after profiling proves it matters |
Practice
Define type Age = range[0 .. 130] and one procedure that accepts it. Then try to pass 200 from a variable to see the check fire, and finally create a distinct wrapper type Years = distinct Age to observe that identical representations are still separate types. Continue with Control Flow.