The Nix expression language has been dynamically typed since its inception, and this has been a persistent frustration for anyone building non-trivial configurations. A new project called typenix takes an unusual approach: rather than building a custom type checker from scratch or replacing Nix with a typed alternative, it uses TypeScript’s type system as the foundation.
The core problem with Nix’s type system
Nix is purely functional and lazy, but it’s also dynamically typed. Every value in Nix belongs to one of a small set of types: strings, integers, floats, booleans, null, lists, attribute sets, functions, paths, and derivations. The language itself provides no mechanism for static type annotations or checked interfaces.
This matters most when you’re working with the NixOS module system. The module system lets you declare options using a runtime type library (types.int, types.str, types.listOf types.package, and so on), and it validates values when your configuration is evaluated. But that validation happens at runtime, after a potentially expensive evaluation pass, and the error messages from type mismatches are often cryptic. A typical module option declaration looks like this:
{
options.services.myApp = {
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Port to listen on";
};
extraFlags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
};
};
}
The type information exists at runtime but is invisible to static tooling. Your editor has no way to know that config.services.myApp.extraFlags is a list of strings without evaluating the entire configuration.
Prior art: Nickel and PureNix
The most prominent typed alternative to Nix is Nickel, developed by Tweag. Nickel implements gradual typing with a contract system: you annotate values with types or contracts, and violations are caught at the point where a contract is applied rather than deep inside a computation. Nickel’s design is principled and addresses many of the same use cases as Nix, but it’s a new language rather than a type system for Nix itself. That means it can’t directly interoperate with existing Nix packages or the NixOS module ecosystem.
PureNix takes a different direction entirely: it compiles PureScript, a Haskell-like language, to Nix, giving you Hindley-Milner type inference and a rich algebraic type system. The generated Nix code is correct by construction, but writing PureScript when you want Nix configuration is a significant cognitive context switch for most users.
The nil language server provides some type inference through an LSP implementation. It can infer types for many expressions and provide completions and diagnostics, but its inference is necessarily limited by Nix’s dynamic nature and the complexity of the module system. nil is useful, but it’s not a full type system.
Why TypeScript’s type system is a reasonable fit
TypeScript’s type system is structurally typed and extraordinarily expressive. It supports mapped types, conditional types, template literal types, variadic tuple types, and an infer keyword that enables type-level computation. Combined with union and intersection types, TypeScript’s checker is capable of encoding remarkably complex invariants. The TypeScript type system is, in a meaningful sense, Turing-complete, a property that projects like ts-toolbelt and the type-challenges repository have explored at length.
Critically, TypeScript’s structural typing maps well to Nix’s attribute sets. In Nix, an attribute set { foo = 1; bar = "hello"; } is essentially a record, and TypeScript’s object types are structural records with the same semantics. Where Nix has attrset.foo, TypeScript has obj.foo. This structural equivalence makes TypeScript a natural type-level mirror for Nix’s most common data structure.
The typenix approach
typenix uses TypeScript’s type checker as a backend for Nix type checking, encoding Nix’s type structure as TypeScript types rather than building a custom type checker or a new language runtime. The approach avoids changes to the Nix toolchain itself, gets TypeScript’s type inference engine for free, and benefits from TypeScript’s mature editor tooling.
The key mechanism is conditional types with infer, which allows TypeScript to track type relationships through complex transformations. A simplified illustration of how Nix module option types could be encoded:
type NixType =
| { kind: 'int' }
| { kind: 'str' }
| { kind: 'bool' }
| { kind: 'listOf'; elementType: NixType }
| { kind: 'attrsOf'; valueType: NixType }
| { kind: 'submodule'; options: Record<string, NixOption> };
type NixOption<T extends NixType = NixType> = {
type: T;
default?: NixValue<T>;
description?: string;
};
type NixValue<T extends NixType> =
T extends { kind: 'int' } ? number :
T extends { kind: 'str' } ? string :
T extends { kind: 'bool' } ? boolean :
T extends { kind: 'listOf'; elementType: infer E extends NixType } ? Array<NixValue<E>> :
T extends { kind: 'attrsOf'; valueType: infer V extends NixType } ? Record<string, NixValue<V>> :
never;
When you declare a module option with type: { kind: 'listOf', elementType: { kind: 'str' } }, TypeScript resolves NixValue<typeof option.type> to string[]. Any attempt to assign an integer or a bare string where a list is expected is caught statically, before any Nix evaluation occurs.
Comparison to other type-embedding approaches
Using one language’s type system to type-check another is not unique to typenix. GraphQL Code Generator translates GraphQL schemas into TypeScript types, turning schema definitions into static guarantees on query results. Kysely encodes SQL table schemas as TypeScript types so that query shapes are checked at compile time. In both cases, the TypeScript compiler acts as the type-checking engine for a domain that isn’t TypeScript at all.
Dhall is perhaps the closest prior art in the configuration space. Dhall is a typed configuration language that can output JSON, YAML, or Nix, and its type system was designed from scratch for configuration use cases. The advantage of typenix’s approach over Dhall is that TypeScript’s type system is already familiar to a large audience and comes with a mature tooling ecosystem. The trade-off is that TypeScript’s type system was designed for JavaScript, not for package management or configuration, so some Nix concepts may require awkward encoding.
Limitations and trade-offs
TypeScript’s type system has real limitations here. It doesn’t have dependent types, which means certain Nix patterns, such as an attribute set whose shape depends on the value of another option, can’t be fully expressed statically. TypeScript also enforces limits on recursive type instantiation depth, which can surface as errors when encoding deeply nested module hierarchies.
There’s also a workflow question. Using typenix means maintaining TypeScript types alongside Nix expressions, or generating one from the other. If the type definitions drift from actual Nix behavior, the type checker produces false confidence rather than genuine safety. This is the same problem that any manually maintained type binding system faces, and it’s worth evaluating carefully before committing to the approach in a large configuration.
The NixOS module system’s support for option merging, priority overrides, and submodule composition is notoriously difficult to model statically. Whether typenix can handle real-world module hierarchies with the full range of merging semantics is an open question.
The broader context
The Nix ecosystem has been grappling with type safety for years, and the solutions have generally split into two camps: replace Nix with a typed alternative, or add types through static analysis. typenix represents a third path, using an existing mature type system as a host.
Whether this approach gains traction depends on how well the TypeScript encoding covers real Nix use cases, and on whether the community’s preferences align with TypeScript. A significant portion of the NixOS user base comes from functional programming backgrounds and may prefer Nickel’s purpose-built type system or PureNix’s Hindley-Milner inference. But for developers who already work in TypeScript and want static guarantees in their NixOS configurations without learning a new type theory, typenix could be a practical entry point.
The underlying idea has merit: TypeScript’s structural typing is a reasonable semantic match for Nix’s attribute-set-heavy design, and TypeScript’s conditional types give enough expressive power to model the module system’s type constructors. The project is early, but the architectural bet is coherent.