Data Types and String Interpolation
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 --jsonOutput:
{"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.nixOutput (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
toStringbefore 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.typeOfwhen a merge “looks like” the wrong kind.
Try this
nix eval --expr 'builtins.typeOf (toString 42)'→"string".- Case 4: add
toString, re-eval. nix eval --expr 'let p = 1; in "${p}"'and save the coerce error.- Compare
"${./types_check.nix}"withtoString ./types_check.nix— both store paths, slightly different spelling rules; pick one style and keep it.