Data Types and String Interpolation

Updated

September 12, 2026

Data Types and String Interpolation

Nix checks types when a value is used, not at parse time. The boring default is: know the six primitives, interpolate with ${…}, toString numbers, and treat unquoted paths as store copies.

Mental model

Type Examples
int 42, -1
float 3.14
bool true, false
string "desk", ''multi''
path ./config.nix, /etc/os-release
null null

Also later: lists, attrsets, functions, derivations.

"${port}" is an error if port is an int. "${toString port}" is the boring spelling.

'' … '' is a multi-line string. Leading indentation of the closing '' is stripped.

Unquoted ./file interpolated into a string copies file into /nix/store and pastes that path. Quoted "./file" is just characters.

Worked examples

Case 1: builtins.typeOf

Save as types_check.nix:

# types_check.nix
{
  intType = builtins.typeOf 100;
  floatType = builtins.typeOf 3.5;
  boolType = builtins.typeOf true;
  stringType = builtins.typeOf "desk";
  pathType = builtins.typeOf ./types_check.nix;
  nullType = builtins.typeOf null;
}
nix eval --file types_check.nix --json

Output:

{"boolType":"bool","floatType":"float","intType":"int","nullType":"null","pathType":"path","stringType":"string"}

Case 2: Interpolation and indented strings

Save as interpolation.nix:

# interpolation.nix
let
  appName = "desk-agent";
  port = 9090;
  logDir = "/var/log/desk";
  endpoint = "http://127.0.0.1:${toString port}";
  systemdService = ''
    [Unit]
    Description=${appName} Service

    [Service]
    ExecStart=/bin/false
    Environment=ENDPOINT=${endpoint}
    StandardOutput=append:${logDir}/output.log
  '';
in
{
  inherit endpoint systemdService;
}
nix eval --file interpolation.nix --apply 'x: x.endpoint'

Output:

"http://127.0.0.1:9090"
nix eval --file interpolation.nix --apply 'x: x.systemdService'

The unit text has no leftover leading spaces from the Nix indent. That is why '' exists for systemd snippets.

Case 3: Path vs string

nix eval --expr 'let f = ./types_check.nix; in "${f}"'

Output (shape):

"/nix/store/…-types_check.nix"
nix eval --expr '"./types_check.nix"'

Output:

"./types_check.nix"

The first copied bytes into the store (eval of a path). The second is a string you could pass to a builder as a name, not as content.

Case 4: Integer interpolation fails

Save as bad_interp.nix:

# bad_interp.nix
let port = 8080; in "listen ${port}"
nix eval --file bad_interp.nix

Output (shape):

error: cannot coerce an integer to a string

Fix: "listen ${toString port}".

Case 5: null is not ""

nix eval --expr 'builtins.typeOf null'
nix eval --expr 'null == ""'

Output:

"null"
false

or on attrsets (set.x or 3) is not the same as null. Missing attr ≠ null attr.

The trap

The trap is quoting a path (src = "./src") and wondering why the builder cannot see your files. Unquoted ./src is the FOD/source. A string is letters.

The other trap is interpolating a derivation into a string at eval ("${pkgs.hello}") — that forces a build (or at least a path). Fine in a module; surprising in a “just eval” scratch file.

The boring rule

  • toString before interpolating ints (and most non-strings).
  • ''…'' for scripts and units.
  • Unquoted paths when Nix should hash the file.
  • Quoted paths when you mean a literal string.
  • builtins.typeOf when a merge “looks like” the wrong kind.

Try this

  1. nix eval --expr 'builtins.typeOf (toString 42)'"string".
  2. Case 4: add toString, re-eval.
  3. nix eval --expr 'let p = 1; in "${p}"' and save the coerce error.
  4. Compare "${./types_check.nix}" with toString ./types_check.nix — both store paths, slightly different spelling rules; pick one style and keep it.