typenix: Full typing for Nix based on TypeScript

Source: github.com
44 points by knl 16 hours ago on lobsters | 5 comments

Full TypeScript-grade typing for the Nix language — autocomplete, type errors, hover, go-to-definition — directly in .nix files with no transpilation step.

Parses and type-checks all 42,298 nixpkgs files in 13 seconds without crashing -- and where types exist or can be automatically inferred by TypeScript, they're correct.

TypeNix demo

Example

# @ts: { lib: Lib; stdenv: Stdenv; [key: string]: any }
{ lib, stdenv }:

let
  version = lib.concatStringsSep "." [ "1" "0" "0" ];
  isLinux = stdenv.hostPlatform.isLinux;
  greeting = lib.optionalString isLinux "Hello from Linux!";

  # ❌ error TS2345: Argument of type 'number' is not assignable
  #    to parameter of type 'boolean'.
  bad = lib.optionalString 42 "oops";
in
stdenv.mkDerivation {
  pname = "example";
  inherit version;
  src = ./.;
}

See examples/starter/ for a runnable project.

Installation

VSCode extension:

code --install-extension $(nix build github:ryanrasti/typenix#vscode-extension --print-out-paths)/typenix.vsix

Neovim:

Put this in lsp/typenix.lua at the root of your Neovim config:

---@type vim.lsp.Config
return {
	cmd = function(dispatchers)
		local cmd = "typenix"
		return vim.lsp.rpc.start({ cmd, "--lsp", "--stdio" }, dispatchers)
	end,
	root_markers = { "flake.nix", ".git" },
	filetypes = {
		"nix",
		"nixts",
	},
}

Then enable it as any other lsp. For nixts filetype for .nix.d.ts files:

vim.filetype.add({
	pattern = {
		[".*/*.nix.d.ts"] = "nixts",
	},
})
vim.treesitter.language.register("typescript", { "nixts" })

Run directly:

# needs a tsconfig.json pointing at your .nix files
echo '{"include": ["**/*.nix"]}' > tsconfig.json

nix run github:ryanrasti/typenix -- --noEmit

How It Works

TypeNix is a fork of tsgo (the TypeScript compiler in Go). When it encounters a .nix file:

  1. tree-sitter-nix parses it
  2. The tree-sitter AST is converted into the same TypeScript AST nodes that the TS parser produces
  3. The standard TypeScript binder, checker, and LSP work essentially unchanged
.nix file → tree-sitter-nix → TS AST → binder → checker → LSP
.ts file  → TS scanner/parser → TS AST → binder → checker → LSP
                                  ↑
                        same types, same pipeline

The result: the full TypeScript type system applied to Nix — with special handling for:

  • Fixed-point/overrides: class annotation converts (self: { ... }) patterns into a TypeScript class, with full self-referential typing
  • Existing :: type annotations from nixpkgs lib/ automatically parsed and used
  • ./foo.nix paths carry their import type, are hoverable and followable via LSP
  • Bundled types for Lib, Stdenv, Platform, Derivation, and all pkgs/by-name packages

Type Annotations

Use # @ts: comments to annotate nix expressions with TypeScript types:

# @ts: { lib: Lib; stdenv: Stdenv; [key: string]: any }
{ lib, stdenv }:

Available types (no imports needed):

  • Nixpkgs — top-level pkgs object
  • Lib — all nixpkgs lib functions (lib.concatStringsSep, lib.optionalString, etc.)
  • StdenvmkDerivation, hostPlatform, cc, etc.
  • PlatformisLinux, isDarwin, system, etc.
  • Derivation — standard derivation output type

Building

nix build .#typenix           # CLI binary
nix build .#vscode-extension  # VS Code extension (includes binary)

Current State

TypeNix is a proof of concept. It is usable on real nix/nixpkgs code:

  • Typing for builtins, lib, stdenv.mkDerivation, import, many packages on the Nixpkgs type
  • ~50 type errors remaining in nixpkgs itself (down from thousands)
  • Fixed-point self-references translated as TypeScript classes allowing for typed self-reference
  • VS Code extension with hover, go-to-definition, autocomplete

Limitations:

  • Many places in nixpkgs need explicit typing to be useful (right now they are implicitly any)
  • noImplicitAny: false in tsconfig.json: ~1k suppressed errors from circular references and variations on the fixed-point structure
  • pkgs/by-name entries are autogenerated — fine-grained types require actually typing the individual files

The above are known, scoped, fixable problems.

Contributing

The best place to start are PRs for the above or other ergonomic fixes. PRs should be:

  • Tested
  • Scoped to a single fix / feature

Issues without an accompanying PR are handled on a best-effort basis.

Why

Nix (the model) vs. Nix (the language):

  • Nix (the model): the only sane way to handle system dependencies. 100% the right approach.
  • Nix (the language): a bespoke runtime that served the model well, but suffers from a lack of modern tooling and is unfamiliar to most developers.

Nix (the language) is holding Nix (the model) back from widespread adoption. TypeNix is a step to unleash it.

What's Next

TypeNix is the first step in unifying the best of the Nix and TypeScript ecosystems. If you are interested follow on X or Github.

License

Apache-2.0 (inherited from typescript-go)