TypeScript declarations
The build can ship a .d.ts next to your .node binary so JavaScript users get full IntelliSense and type checking. There are three modes, controlled by the .dts field of .npm:
Hand-written (.file)
Recommended for libraries published to npm. A hand-written .d.ts gives you the full TypeScript surface: overloads, conditional types, branded types, JSDoc comments, and the freedom to keep your public API stable independent of internal Zig refactors.
The build copies this file as-is to index.d.ts next to the .node binary, and into the published npm package. You write what your users should see; the bridge does not interfere.
Auto-generated (.auto)
The build runs a comptime walk of your module and emits a .d.ts from the Zig signatures. Useful for prototypes, internal addons, or as a starting point that you check into src/index.d.ts and then edit.
What you get:
- Named declarations for every generated JavaScript named export, plus a typed default addon object with the same members as
export default binding. - Every
pub fnbecomes a named function and a method on the default addon object. - Every
pub const(with a JS-mappable value) becomes a typed property. - Nested namespaces become nested objects.
- Argument and return types come from the Zig signatures.
- Errors, optionals, enums, and structs are translated.
- Fixed arrays, including
[N]u8, become JavaScript numeric arrays such asnumber[]; byte arrays are not strings. - Only a contiguous optional suffix becomes TypeScript
?parameters. A non-trailing Zig optional remains required and acceptsundefined | nullexplicitly. - All-default struct parameters follow the same suffix rule and accept explicit
undefined; quoted enum tags and struct fields are emitted as escaped TypeScript literals when they cannot be written as bare identifiers. - Enum tags and struct fields that collide after snake_case-to-camelCase conversion are rejected instead of producing ambiguous or invalid declarations.
What is intentionally unknown:
napi.Valbecomesunknown. There is no JS type to infer from a passthrough.napi.Callbackbecomes(...args: unknown[]) => unknown.- A struct with custom
toJsorfromJsbecomesunknownunless it declares a trustedpub const _dts = "..."TypeScript type fragment._dtsis a bidirectional contract:fromJsmust accept it andtoJsmust produce it. For asymmetric one-way conversions, keepunknownor use a handwritten declaration file with distinct input and output types.
These are escape hatches by design. Tighten the Zig signature and the generated type tightens with it. For example, []f64 generates number[] instead of unknown[].
A common workflow: start with .auto, run napikit build, copy the generated file out of zig-out/lib/<name>.d.ts into src/index.d.ts, edit it to taste, and switch the build to .{ .file = ... }.
None (.none)
No .d.ts is emitted. JavaScript users get no autocomplete or type checking for the addon. This is the default.
For a development build, the project-root copy is marked as build-owned even when its source is a hand-written .{ .file = ... }. Later source edits replace that derived copy, and switching from .auto or .{ .file = ... } to .none removes the stale generated zig-out/lib/<name>.d.ts and owned project-root <name>.d.ts. A legacy unmarked copy is migrated when it still matches the previous installed declaration; arbitrary user declarations are not touched.
For a release package, .none also omits the types field and the index.d.ts entry from files. If the package previously used .auto or .{ .file = ... }, the next full napikit build --release removes the stale build-owned npm/<name>/index.d.ts and its managed manifest entry. Other files and user-managed manifest fields are preserved.
Rule of thumb
If your addon will be installed by other people, hand-write your .d.ts. If it is internal, .auto is fine.
Calling the generator yourself
The generator is also exposed as a public API: napi.dts.generate(Module) returns the comptime .d.ts string for Module. The build option .auto uses this internally; you do not normally call it directly.