Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Stubs

TLDR: .tix files declare types for external Nix code — like TypeScript’s .d.ts files. Tix ships with built-in stubs for common nixpkgs functions, and you can generate stubs from NixOS/Home Manager option trees.

What are stubs?

Nix’s import system makes full-program inference impractical. You’re not going to infer all of nixpkgs. Stubs let you declare types for code that lives outside your project.

tix inspect my-file.nix --stubs ./my-stubs/

--stubs takes a file or directory (recursively finds .tix files). Can be passed multiple times. Built-in stubs load by default (--no-default-stubs to disable).

Writing stubs

Basic syntax

# Line comments

# Type aliases — lowercase vars are implicitly generic
type Derivation = { name: string, system: string, ... };
type Nullable = a | null;

# Value declarations
val mkDerivation :: { name: string, src: path, ... } -> Derivation;

# Modules — nest values and create type aliases from the module name
module lib {
  val id :: a -> a;
  module strings {
    val concatStringsSep :: string -> [string] -> string;
  }
}
# ^ creates type alias "Lib" = { id: a -> a, strings: { concatStringsSep: ... }, ... }

Type expressions

Same syntax as doc comment annotations — see Type Annotations.

Modules create type aliases

When you write module foo { ... }, tix auto-generates a type alias Foo (capitalized) representing the attrset type of that module’s contents. This is how Lib and Pkgs work in the built-in stubs.

Top-level val declarations

Top-level val declarations (outside any module) provide types for unresolved names automatically — no annotation needed in your Nix code:

val mkDerivation :: { name: string, ... } -> Derivation;
# No annotation needed — mkDerivation is resolved from stubs
mkDerivation { name = "my-pkg"; src = ./.; }

Built-in stubs

Tix ships with stubs for common nixpkgs functions. These are compiled into the binary and loaded by default. They cover:

  • Pkgs: mkDerivation, stdenv.mkDerivation, fetchurl, fetchFromGitHub, runCommand, writeText, etc.
  • Lib: ~500 declarations covering strings, lists, attrsets, trivial, fixedPoints, options, modules, fileset, filesystem, path, sources, versions, debug, generators, customisation, meta, asserts, gvariant, network, and more. Generated from noogle.dev data.
  • Derivation: type alias for { name: string, system: string, builder: path | string, ... }

Use --no-default-stubs if you want to replace them entirely with your own.

Built-in context stubs

When used in a tix.toml context, @-prefixed stub names refer to built-in context sources:

StubSourceProvides
@nixosCompiled-in NixOS context stubsconfig, lib, pkgs, options, modulesPath
@home-managerCompiled-in Home Manager context stubsconfig, lib, pkgs, osConfig
@callpackageDerived from Pkgs module aliasAll fields from module pkgs in the built-in stubs (stdenv, fetchurl, lib, mkDerivation, etc.)

@callpackage doesn’t require a separate stub file. It extracts the fields of the Pkgs type alias (created by module pkgs { ... } in the built-in stubs) and provides them as context args. This is the same mechanism that any module foo { ... } declaration uses: @foo resolves to Foo.

Custom context stubs from a file

For module systems not shipped as a built-in (e.g. flake-parts, devenv, custom NixOS-like systems), point a context at a local .tix file. The file can use either top-level val declarations or a module block — each field becomes a context arg:

[context.flake-parts]
includes = ["modules/**/*.nix"]
stubs = ["./flake-parts.tix"]
# flake-parts.tix
module flakeparts {
    val config :: { perSystem: a -> b, ... };
    val lib :: Lib;
}

Files matching modules/**/*.nix see config and lib as typed lambda parameters. When a top-level val and a module field share a name, the top-level val wins (more explicit). Tix logs a warning if a stub file produces zero context args — usually a sign that the file contains only type aliases with no val or module declarations.

Generating stubs from NixOS/Home Manager

Tix can generate stubs from NixOS options, Home Manager options, and nixpkgs package sets. This gives you typed access to config, lib, pkgs, and other parameters in your Nix files.

From a flake

# NixOS options
tix stubs generate nixos --flake . --hostname myhost -o nixos.tix

# Home Manager options
tix stubs generate home-manager --flake . --username jr -o hm.tix

From nixpkgs directly

tix stubs generate nixos --nixpkgs /path/to/nixpkgs -o nixos.tix

Options

FlagDescription
--flake PATHFlake directory to evaluate
--hostname NAMENixOS hostname (required if multiple configurations)
--username NAMEHome Manager username (required if multiple configurations)
--nixpkgs PATHPath to nixpkgs (default: <nixpkgs> from NIX_PATH)
--from-json PATHRead pre-computed option tree JSON instead of running nix eval
-o, --output PATHOutput file (default: stdout)
--max-depth NMaximum recursion depth for option tree walking (default: 8)
--descriptionsInclude option descriptions as doc comments

Generating pkgs stubs

For callPackage-style files, you can auto-generate val declarations for all of nixpkgs:

tix stubs generate pkgs -o generated-pkgs.tix

This evaluates nixpkgs and classifies each attribute:

  • Derivations become val hello :: Derivation;
  • Non-derivation attrsets become val xorg :: { ... };
  • Functions become val callPackage :: a -> b;

Sub-package-sets like llvmPackages, python3Packages, and xorg that have recurseForDerivations = true are recursed into and emitted as nested modules:

module pkgs {
  val hello :: Derivation;
  module python313Packages {
    val numpy :: Derivation;
    val pandas :: Derivation;
  }
  val python3Packages :: Python313Packages;
  val writeText :: a -> b;
}

Alias detection: Nixpkgs uses dontRecurseIntoAttrs on alias package sets (e.g. python3Packages = dontRecurseIntoAttrs python313Packages). When a non-recursed attrset has recurseForDerivations explicitly set to false and its builtins.attrNames matches a recursed sibling, tix emits a type alias reference (val python3Packages :: Python313Packages;) instead of an opaque { ... }. This gives alias sets the same typed fields as their targets.

Use --max-depth to control recursion depth (default: 1). Higher values give more coverage but increase eval time — python3Packages alone has ~10k attributes. Use --max-depth 0 for flat output (no recursion).

The output is a module pkgs { ... } block that merges with the hand-curated module pkgs in the built-in stubs, extending the Pkgs type alias with thousands of additional fields. Since @callpackage derives its context from Pkgs, the generated packages are picked up automatically.

# Generate from specific nixpkgs
tix stubs generate pkgs --nixpkgs /path/to/nixpkgs -o generated-pkgs.tix

# Recurse deeper into sub-package-sets
tix stubs generate pkgs --max-depth 2 -o generated-pkgs.tix

# Flat output (no sub-package-set recursion, like pre-v0.x behavior)
tix stubs generate pkgs --max-depth 0 -o generated-pkgs.tix

# From pre-computed JSON (for reproducibility or CI)
tix stubs generate pkgs --from-json classified.json -o generated-pkgs.tix

Load the generated file via --stubs or the stubs config key:

stubs = ["./generated-pkgs.tix"]

[context.callpackage]
includes = ["pkgs/**/*.nix"]
stubs = ["@callpackage"]

Using generated stubs with tix.toml

Once generated, point your tix.toml at them. See Configuration.

Custom module systems (flake-parts, devenv, nix-darwin, …)

For any module system that ships its own evalModules-style API, you can generate typed stubs the same way NixOS and HM do. The flow is iterate with the CLI first, promote to tix.toml once happy.

1. Iterate manually with the CLI

tix stubs generate module wraps an arbitrary options expression in the generic extractor and emits a .tix. This gives immediate feedback with no LSP restart loop.

tix stubs generate module \
  --name flake-parts \
  --options-expr '(inputs.flake-parts.lib.evalFlakeModule { self = self; } { imports = [ ]; }).options' \
  --context-arg config \
  --context-arg lib \
  --context-arg pkgs \
  -o flake-parts.tix

Inspect the output, tweak the expression, re-run. The generated file contains a type FlakePartsConfig = { ... } alias and one val declaration per --context-arg. Types are assigned by name, not position: configFlakePartsConfig (the options tree), libLib, pkgsPkgs, any other name → { ... }. Order doesn’t matter — reordering the args produces the same output.

Optional two-step debugging:

# Inspect the raw options tree first.
nix eval --json --impure --expr '...' > options.json
tix stubs generate module --from-json options.json --name flake-parts ... -o flake-parts.tix

2. Promote to tix.toml

Once the CLI command produces a good stub, copy the expression into [stubs.generate.systems.<name>]. The LSP runs the same pipeline on startup and writes <name>.tix into its cache, so @<name> just works as a context stub.

[stubs.generate]
nixpkgs = { expr = "(builtins.getFlake (toString ./.)).inputs.nixpkgs" }

[stubs.generate.systems.flake-parts]
options_expr = '''
  (inputs.flake-parts.lib.evalFlakeModule
    { self = self; } { imports = [ ]; }).options
'''
context_args = ["config", "lib", "pkgs"]

[context.flake-parts]
includes = ["flake-modules/**/*.nix"]
stubs = ["@flake-parts"]

3. Typing extra module args precisely

Args beyond config/lib/pkgs (e.g. flake-parts withSystem, inputs, self; devenv inputs') are typed as { ... } in the generated stub. That’s enough for the checker to recognise the name, but calls against it aren’t checked — withSystem "x86_64-linux" foo just returns an opaque attrset.

To give specific args precise types, write a companion .tix and layer it after the generated stub in the context’s stubs array. Later entries override earlier ones on name collisions:

# flake-parts-extras.tix
val withSystem :: string -> a -> a;
val inputs :: { self: { ... }, nixpkgs: { ... }, ... };
[stubs.generate.systems.flake-parts]
options_expr = "..."
context_args = ["config", "lib", "pkgs", "withSystem", "inputs"]

[context.flake-parts]
includes = ["flake-modules/**/*.nix"]
stubs = ["@flake-parts", "./flake-parts-extras.tix"]  # extras win

The generated stub still types config/lib/pkgs; the extras file refines withSystem and inputs to precise types. Top-level val decls from all files in stubs are unioned into the context, so extras.tix can also add new arg names not declared in context_args at all — handy for iterating without re-running tix stubs refresh.

4. After edits

The LSP generates custom-system stubs once at startup. If you change the options_expr, context_args, or any module referenced from them, run tix stubs refresh and restart the LSP.

Refreshing generated stubs

Runtime-generated stubs (from [stubs.generate] in tix.toml) are cached in ~/.cache/tix/store-stubs/. The cache keys on the inputs that feed into nix build — primarily the nixpkgs + home-manager store paths — so bumping pinned inputs via flake.lock invalidates the cache naturally.

The cache does not content-hash your own files. When you edit anything that the stub pipeline depends on (e.g. a Nix expression in [stubs.generate], future user-module configurations), the cache keeps serving stale stubs until you invalidate it manually:

tix stubs refresh

This clears the cache directory; the next run of the LSP or tix check will re-invoke nix build. Restart the LSP afterward — the LSP only runs stub generation at startup today.

Source annotations

Stub declarations can carry @source annotations that link back to the original source file. When present, go-to-definition in the LSP jumps directly to the nixpkgs (or home-manager) source instead of landing in the generated .tix file.

Syntax

@source <source-id>:<relative-path>:<line>:<column>

For example:

@source nixpkgs:lib/trivial.nix:61:8
val id :: a -> a;

@source can appear before val, type, and module declarations, as well as on individual attrset fields (used in NixOS/Home Manager option stubs).

How it works

When stubs are generated with --source-root nixpkgs=/nix/store/...-source, absolute Nix store paths from builtins.unsafeGetAttrPos are stripped against the root to produce relative paths. At LSP startup the source root is resolved (typically from the flake lock) so that go-to-definition can open the real file.

When using [stubs.generate] in tix.toml, source roots are passed automatically — no manual --source-root flags needed.

Using stubs in your code

Assign stub types to imports via doc comments:

let
  /** type: lib :: Lib */
  lib = import <nixpkgs/lib>;

  /** type: pkgs :: Pkgs */
  pkgs = import <nixpkgs> {};

  greeting = lib.strings.concatStringsSep ", " ["hello" "world"];
  drv = pkgs.stdenv.mkDerivation { name = "my-package"; src = ./.; };
in
{ inherit greeting drv; }

Now lib.strings.concatStringsSep is typed as string -> [string] -> string, and drv is typed as Derivation.