Types & Values

Nim is statically typed, and the compiler uses types to catch whole categories of bugs before your program runs. This lesson introduces the primitive types, the inference you get for free, the types that encode constraints (ranges, distinct), and the difference between a checked conversion and an unchecked cast.

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.
SwitchRange and overflow checksIntended use
nim cEnabledEveryday development
nim c -d:releaseEnabledBenchmarks and releases
nim c -d:dangerDisabledOnly 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.