Nix has been dynamically typed since its inception in 2003. For a language that now underpins tens of thousands of packages in nixpkgs and entire operating system configurations through NixOS, that’s a substantial surface for silent runtime failures. The community has lived with this by convention, documentation, and careful code review. typenix tries a different path: grafting TypeScript’s type system onto Nix rather than inventing a new one.
The decision to borrow TypeScript rather than design something bespoke is worth examining carefully, because it shapes every trade-off the project makes.
What Nix Actually Looks Like Without Types
Nix the language is small. Its primitives are integers, floats, booleans, strings, paths, null, lists, attribute sets (attrsets), functions, and derivations. Of these, the attrset is the load-bearing type. Almost everything in nixpkgs is an attrset: packages are attrsets with name, src, buildInputs, and dozens of optional fields; overlays are functions from attrsets to attrsets; NixOS modules are attrsets with options and config sub-attrsets.
Because Nix is lazily evaluated and dynamically typed, a function that expects an attrset with a pname field will simply fail at evaluation time if you pass something else. There’s no compiler pass that catches the mistake earlier. For a maintainer browsing nixpkgs, the signature of stdenv.mkDerivation lives in documentation and convention, not in anything the language enforces.
The existing workaround in NixOS is lib.types, a runtime type-checking system embedded in the NixOS module machinery. You declare option types like this:
options.services.myService = {
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Port to listen on";
};
};
lib.types includes primitives (int, str, bool, path), composites (listOf, attrsOf, nullOr, oneOf), and coercions. It works, but it’s entirely a runtime mechanism: the type is checked when NixOS evaluates your configuration, not when you write it. There’s no static analysis, no IDE feedback, and no way to express types on arbitrary Nix functions outside the module system.
Several projects have tried to close this gap. Nickel is the most complete alternative: a configuration language designed from scratch with gradual typing, contracts, and proper type inference. Nickel is not Nix, though, and migrating nixpkgs to it would be a larger project than most communities could realistically undertake. There have also been experiments with adding a type checker directly to Nix itself, but nothing has shipped in the mainline.
Why TypeScript’s Type System
Typenix’s central bet is that TypeScript’s structural type system is already a good model for Nix attrsets, and that reusing it means getting a Turing-complete, battle-tested type checker with mature tooling for free.
The mapping is intuitive on the surface. A Nix attrset:
{ pname = "hello"; version = "2.12"; buildInputs = []; }
corresponds naturally to a TypeScript object type:
type Derivation = {
pname: string;
version: string;
buildInputs: Derivation[];
// ...
};
TypeScript’s optional properties (foo?: string) map to optional attrset fields. Union types (string | string[]) capture the Nix pattern where a value can be one thing or another, which is extremely common in nixpkgs function signatures. Intersection types (A & B) model attrset merges via the // operator or lib.mkMerge.
Structural typing specifically is the right fit here. Nix has no nominal types; there are no classes, no interfaces in the OOP sense. When a Nix function accepts a “derivation”, it means “any attrset with at least these fields”. TypeScript’s structural model, where a type is satisfied by anything with the required shape, mirrors this precisely. A nominal type system would require explicit declarations of conformance that Nix’s semantics don’t support.
TypeScript’s type system is also unusually expressive for a mainstream language. Conditional types, template literal types, mapped types, and infer expressions make it possible to encode quite complex constraints. That expressiveness matters for Nix because nixpkgs has real complexity: mkDerivation has a large optional surface, overriding attributes in derivations follows particular patterns, and overlays compose in ways that are hard to track without types.
The Gradual Typing Angle
Gradual typing, the idea of adding types incrementally to a dynamic language rather than requiring a full conversion, is the practical playbook here. Python did this with PEP 484 and mypy, then pyright. Ruby got Sorbet and RBS. PHP has Hack. The pattern is consistent: you start with a type-aware layer that coexists with untyped code, progressively annotate the critical paths, and let the tooling surface errors.
Typenix fits into this tradition. You can annotate the parts of your Nix code you care about and leave the rest untyped, similar to how Python type stubs work for third-party libraries that haven’t added annotations yet. This is much more pragmatic than requiring nixpkgs to be fully typed before any benefit is realized.
The challenge with gradual typing in Nix is that Nix’s laziness interacts poorly with type checking in ways that don’t arise in Python or Ruby. In a strict language, evaluating an expression produces a value you can type-check. In a lazy language, a thunk is not evaluated until needed, which means type errors can be latent in code paths that are never triggered during development but explode in production. A static type checker doesn’t evaluate anything, so it catches these cases, but the laziness also means that some Nix idioms produce values whose types are only knowable after evaluation.
Consider a function that conditionally builds an attrset:
let
base = { name = "foo"; };
extended = if someCondition then base // { extra = 42; } else base;
in extended
The type of extended is { name: string; extra?: number } in the conditional-true branch but { name: string } in the false branch. A union type captures this, but as Nix programs grow more complex, these conditional shapes proliferate and unions become unwieldy. TypeScript’s conditional types and discriminated unions are the tools for managing this, but they require careful annotation.
What This Means for nixpkgs at Scale
nixpkgs has over 100,000 packages as of 2026. Its lib library has hundreds of functions. Maintaining this without types relies on contributor discipline, documentation, and review. When a function signature changes or an attrset gains a new required field, propagating that information across the codebase is manual work.
Type stubs for nixpkgs, generated or manually written, would change this. The lib.attrsets, lib.lists, lib.strings, and lib.trivial namespaces all have well-understood signatures that could be typed accurately. More importantly, the NixOS module system’s options declarations already encode type information in lib.types; a tool that reads those declarations and generates TypeScript type stubs would give typenix coverage over the entire NixOS configuration surface with relatively little manual work.
This is one of the more compelling applications: not typing arbitrary Nix expressions, but specifically typing NixOS module interfaces so that editors can provide autocomplete and error feedback for system configuration. If you mistype services.nginx.virtualHosts."example.com".locations."/".proxyPass as a number instead of a string, you currently find out at nixos-rebuild switch. With typed stubs, you’d find out before you leave your editor.
The TypeScript LSP as a Free Bonus
Using TypeScript as the backend means the TypeScript Language Server becomes available for Nix files, at least for the typed portions. This is a significant practical benefit. The TypeScript LSP is mature, widely supported, and provides hover types, go-to-definition, rename refactoring, and inline error diagnostics. Getting these for Nix through a translation layer is considerably faster than implementing a dedicated Nix language server with equivalent capabilities from scratch.
Existing Nix language server efforts, like nil and nixd, provide some of this already, but without static type information their ability to detect type errors is limited. A typenix-aware editor setup could layer TypeScript’s type checking on top of a Nix language server’s syntax and scoping support.
Where This Fits in the Broader Landscape
The alternatives to typenix represent different positions on the spectrum between “add types to Nix” and “replace Nix with a typed language”.
Nickel sits near the replacement end. It’s a well-designed language with contracts that provide both static and runtime type checking, but adopting it means writing Nickel instead of Nix. The ecosystems don’t share code.
Noogle, Nix’s function documentation search, sits at the other end: it improves discoverability of existing functions without adding enforcement. Useful, but not a type checker.
Projects like nix-types represent community attempts at runtime annotation within Nix itself, closer to the lib.types model.
Typenix occupies a distinct position: it adds static type checking to actual Nix code without requiring a language migration. The cost is that TypeScript’s type system was designed for JavaScript semantics, not Nix semantics. Some Nix concepts, particularly derivations with their builder-specific attributes, fetchurl with its hash requirements, and the import-from-derivation pattern, don’t map cleanly to anything in TypeScript’s model. Those will require either custom type definitions or type-level workarounds.
The Open Question
The real test for a project like typenix is adoption friction. Gradual type systems succeed when the cost of adding annotations to a file is low and the benefit is immediately visible. Python’s type system succeeded in part because mypy could be run on a single file, and tools like MonkeyType could generate type annotations from runtime traces, lowering the initial annotation burden.
For Nix, the question is whether a generator can produce TypeScript type stubs from nixpkgs automatically, covering the 90% case and requiring manual annotation only for the complex patterns. If so, typenix could provide real value across the existing ecosystem without requiring every Nix author to learn TypeScript’s type annotation syntax. If manual annotation is the only path, adoption will be slow and the typed subset of the Nix ecosystem will remain small.
Borrowing TypeScript’s type system for Nix is a pragmatic move that trades some precision for a great deal of existing infrastructure. Whether the mapping is tight enough to be useful at nixpkgs scale is what the project will ultimately prove or disprove.