Extremely fast, smart
.d.tsgeneration with sound type inference &@defaultValuepreservation.
- 🎯 Sound type inference with
@defaultValuepreservation - ⚡ Extremely fast
.d.tsgeneration - 🤖 Cross-platform binary (Zig native + Bun)
- 🔄 Parallel processing support
- 📥 Stdin/stdout support for piping
- 👀 Watch mode for development
- ⚙️ Highly configurable
- ✅ Built-in validation
- 🔌 tsdown integration with ESM, CommonJS and watch support
- 📦 Bundled API and CLI with no required npm dependencies
Note
dtsx works out of the box without isolatedDeclarations — it infers narrow types directly from your source values. That said, enabling isolatedDeclarations is still a good idea as it enforces explicit type annotations at module boundaries, encouraging better type hygiene across your codebase. When enabled, dtsx treats annotations as authoritative, skips initializers for concrete types, and only reads broad containers to preserve @defaultValue documentation.
npm install -D @dtsx/dtsx
# or
bun add -d @dtsx/dtsx@npmjs.com, please allow us to use the
dtsxpackage name 🙏
For the standalone native CLI on macOS or Linux:
brew install stacksjs/tap/dtsx zig-dtsx --project src --outdir dist
The native zig-dtsx command does not require Bun or npm packages.
Note
dtsx was published as @stacksjs/dtsx up to v0.11.36. Install @dtsx/dtsx instead and change imports from @stacksjs/dtsx to @dtsx/dtsx; the API, CLI and config are unchanged. See Migrating from @stacksjs/dtsx.
There are two ways to use dtsx: as a library or as a CLI. Both work out of the box — no isolatedDeclarations required. dtsx infers narrow types directly from your source values. If you do enable isolatedDeclarations, dtsx uses it as a fast path to skip initializer parsing when explicit type annotations are present.
import type { DtsGenerationOptions } from '@dtsx/dtsx' import { generate, processSource } from '@dtsx/dtsx' const options: DtsGenerationOptions = { cwd: './', // default: process.cwd() root: './src', // default: './src' entrypoints: ['**/*.{ts,tsx,mts,cts,vue,stx}'], // default outdir: './dist', // default: './dist' clean: true, // default: false verbose: true, // default: false keepComments: true, // default: true parallel: true, // default: false - process files in parallel concurrency: 4, // default: 4 - number of concurrent workers dryRun: false, // default: false - preview without writing stats: true, // default: false - show generation statistics validate: true, // default: false - validate generated .d.ts files } const stats = await generate(options) console.log(`Generated ${stats.filesGenerated} files in ${stats.durationMs}ms`) // You can also process source code directly: const dtsContent = processSource(` export const greeting: string = "Hello"; export function greet(name: string): string { return greeting + " " + name; } `) console.log(dtsContent) // Output: // export declare const greeting: string; // export declare function greet(name: string): string;
Entrypoints, include patterns, and relative re-exports share normalized native
source paths. Overlapping matches generate each source once, including on
Windows. With outputStructure: 'flat', nested sources emit directly into
outdir; distinct sources with the same declaration filename fail before
cleaning the previous declarations.
Directory barrels and graph entrypoints use the same native path identity as
discovered files, including absolute aliases containing .. segments.
.stx files are declaration entrypoints alongside TypeScript and Vue files.
dtsx reads Vue-style <script server|client> blocks and Blade-style @ts
blocks, then emits an @stacksjs/stx DefineComponent declaration. Props are
derived from typed macros, runtime prop schemas, explicit props as Props
assertions, and legacy $props.name access:
<script server lang="ts">
interface CardProps {
title: string
count?: number
}
const props = withDefaults(defineProps<CardProps>(), { count: 0 })
</script>
<article>{{ props.title }}</article>
This emits Card.d.ts with { title: string; count?: number } as its $props
contract. The same transform feeds the semantic inference path and the
annotation-first isolatedDeclarations path.
Library usage can also be configured using a dts.config.ts (or dts.config.js) file, automatically loaded when running ./dtsx (or bunx dtsx) and when calling generate() unless custom options are provided.
// dts.config.ts (or dts.config.js) export default { cwd: './', root: './src', entrypoints: ['**/*.ts'], outdir: './dist', keepComments: true, clean: true, verbose: true, // Performance options parallel: true, concurrency: 4, // Output options stats: true, validate: true, // Filtering exclude: ['**/*.test.ts', '**/**tests**/**'], importOrder: ['node:', 'bun', '@myorg/'], }
dtsx emits through its native Zig engine by default and falls back to its TypeScript engine where the native library cannot load. The TypeScript engine is kept as the reference implementation: select it with engine: 'typescript', --engine typescript or DTSX_ENGINE=typescript to compare the two on the same source. See Engines.
You may also run:
./dtsx generate # if the package is installed, you can also run # bunx dtsx generate
Generate declaration files using the default options:
dtsx generate
Or use custom options:
# Generate declarations for specific entry points dtsx generate --entrypoints src/index.ts,src/utils.ts --outdir dist/types # Generate declarations with custom configuration dtsx generate --root ./lib --outdir ./types --clean # Use parallel processing for large projects dtsx generate --parallel --concurrency 8 # Preview what would be generated (dry run) dtsx generate --dry-run --stats # Validate generated declarations dtsx generate --validate # Exclude test files dtsx generate --exclude "**/*.test.ts,**/**tests**/**" # Custom import ordering dtsx generate --import-order "node:,bun,@myorg/" dtsx --help dtsx --version
Watch for changes and regenerate automatically:
# Watch with default options dtsx watch # Watch specific directory dtsx watch --root src --outdir dist/types
Process TypeScript from stdin and output declarations to stdout:
# Pipe source code directly echo "export const foo: string = 'bar'" | dtsx stdin # Process a file through stdin cat src/index.ts | dtsx stdin # Chain with other tools cat src/utils.ts | dtsx stdin > dist/utils.d.ts
Basic Options:
--cwd <path>: Set the current working directory (default: current directory)--root <path>: Specify the root directory of the project (default: './src')--entrypoints <files>: Define entry point files. Pass several by repeating the flag or separating them with commas (default:**/*.{ts,tsx,mts,cts,vue,stx})--outdir <path>: Set the output directory for generated .d.ts files (default: './dist')--keep-comments: Keep comments in generated .d.ts files (default: true)--clean: Clean output directory before generation (default: false)--tsconfig <path>: Specify the path to tsconfig.json (default: 'tsconfig.json')
Performance Options:
--parallel: Process files in parallel (default: false)--concurrency <number>: Number of concurrent workers with --parallel (default: 4)
Output Options:
--verbose: Enable verbose output (default: false)--log-level <level>: Log level: debug, info, warn, error, silent (default: 'info')--stats: Show statistics after generation (default: false)--output-format <format>: Output format: text or json (default: 'text')--engine <engine>: Declaration engine,nativeortypescript(default: 'native'; accepted by every command)--progress: Show progress during generation (default: false)--diff: Show diff of changes compared to existing files (default: false)
Validation Options:
--validate: Validate generated .d.ts files against TypeScript (default: false)--continue-on-error: Continue processing if a file fails (default: false)--dry-run: Preview without writing files (default: false)
Filtering Options:
--exclude <patterns>: Glob patterns to exclude (comma-separated)--import-order <patterns>: Import order priority patterns (comma-separated)
To learn more, head over to the documentation.
dtsx generates sound, narrow types with @defaultValue preservation — no isolatedDeclarations flag required, no explicit type annotations needed. Where tsc and oxc silently discard original values when widening types, dtsx preserves them as standard @defaultValue JSDoc so they surface in IDE hover tooltips. All output below is real — same source file, three tools, nothing hand-edited.
In TypeScript, const only makes the binding immutable — object properties and array elements remain mutable. This means const config = { timeout: 5000 } allows config.timeout = 9999, so the declared type must be number, not 5000. All three tools correctly widen mutable container properties. The difference is what happens to the original values:
| Tool | Widened type | Original value preserved? |
|---|---|---|
| dtsx | /** @defaultValue 5000 */ timeout: number |
Yes — via @defaultValue JSDoc |
| tsc | timeout: number |
No — value lost entirely |
| oxc | timeout: number |
No — value lost entirely |
Scalar const bindings are truly immutable — const port = 3000 can never change. All tools keep the literal type:
// Source export const port = 3000 export const debug = true
port |
debug |
|
|---|---|---|
| dtsx | 3000 |
true |
| tsc | 3000 |
true |
| oxc | 3e3 (mangled!) |
boolean |
// Source export const config = { apiUrl: 'https://api.stacksjs.org', timeout: 5000, features: { darkMode: true, notifications: false }, routes: ['/', '/about', '/contact'], }
| Property | dtsx | tsc | oxc |
|---|---|---|---|
apiUrl |
string + @defaultValue 'https://...' |
string |
string |
timeout |
number + @defaultValue 5000 |
number |
number |
darkMode |
boolean + @defaultValue true |
boolean |
boolean |
routes |
string[] |
string[] |
unknown (error) |
Top-level @defaultValue |
full object literal | (none) | (none) |
dtsx output:
/** _ @defaultValue _ ```ts _ { _ apiUrl: 'https://api.stacksjs.org', _ timeout: 5000, _ features: { darkMode: true, notifications: false }, _ routes: ['/', '/about', '/contact'] _ } _ ``` _/ export declare const config: { /** @defaultValue 'https://api.stacksjs.org' */ apiUrl: string; /** @defaultValue 5000 */ timeout: number; features: { /** @defaultValue true */ darkMode: boolean; /** @defaultValue false */ notifications: boolean }; routes: string[] };
tsc and oxc output (values lost):
export declare const config: { apiUrl: string; timeout: number; features: { darkMode: boolean; notifications: boolean }; routes: string[] // oxc errors here };
Defaults on class fields and parameter properties surface the same way, and defaulted parameters keep the types their defaults imply:
// Source export class Client { retries = 3 static version = '1.0.0' constructor(public timeout = 1000) {} } export function connect(host = 'localhost', { ssl = true, poolSize = 10 } = {}) { return { host, ssl, poolSize } } export function format(value: number, digits = 2) { return value.toFixed(digits) }
dtsx output:
export declare function connect(host?: string, { ssl, poolSize }?: { ssl?: boolean | undefined; poolSize?: number | undefined }): { host: string; ssl: boolean; poolSize: number }; export declare function format(value: number, digits?: number): string; export declare class Client { /** @defaultValue 3 */ retries: number; /** @defaultValue '1.0.0' */ static version: string; /** @defaultValue 1000 */ public timeout: number; constructor(timeout?: number); }
tsc infers the same types but drops every default. oxc requires annotations: it reports 5 errors, declares connect as returning {} without its second parameter, and leaves format without a return type.
dtsx replaces broad generic annotations with narrow types inferred from the actual value:
// Source — generic index signature export const conf: { [key: string]: string } = { apiUrl: 'https://api.stacksjs.org', timeout: '5000', }
| Tool | Output |
|---|---|
| dtsx | { apiUrl: 'https://api.stacksjs.org'; timeout: '5000' } |
| tsc | { [key: string]: string } — kept broad, lost all property info |
| oxc | { [key: string]: string } — kept broad, lost all property info |
When you explicitly use as const, all tools should preserve literal types. dtsx handles this correctly:
// Source export const CONFIG = { api: { baseUrl: 'https://api.example.com', timeout: 5000, retries: 3 }, features: { darkMode: true, notifications: false }, routes: ['/', '/about', '/contact'], } as const
dtsx output — every value preserved as a literal, arrays become readonly tuples, no @defaultValue needed (types are already self-documenting):
export declare const CONFIG: { api: { baseUrl: 'https://api.example.com'; timeout: 5000; retries: 3 }; features: { darkMode: true; notifications: false }; routes: readonly ['/', '/about', '/contact'] };
export const promiseVal = Promise.resolve(42)
| Tool | Output |
|---|---|
| dtsx | Promise<42> (resolved values are immutable) |
| tsc | Promise<number> |
| oxc | unknown (error — requires explicit annotation) |
| Declaration | dtsx | tsc | oxc |
|---|---|---|---|
const port = 3000 |
3000 |
3000 |
3e3 |
const debug = true |
true |
true |
boolean |
const items = [1,2,3] |
number[] + @defaultValue |
number[] |
unknown (error) |
config.apiUrl |
string + @defaultValue |
string |
string |
config.timeout |
number + @defaultValue |
number |
number |
config.routes |
string[] |
string[] |
unknown (error) |
conf (generic annotation) |
exact properties | { [key]: string } |
{ [key]: string } |
Promise.resolve(42) |
Promise<42> |
Promise<number> |
unknown (error) |
class field retries = 3 |
number + @defaultValue |
number |
number |
{ ssl = true } = {} parameter |
{ ssl?: boolean | undefined } |
{ ssl?: boolean | undefined } |
dropped (error) |
value.toFixed(digits) return |
string |
string |
missing (error) |
| Value info preserved? | Yes | No | No |
| Errors | 0 | 0 | 3 (+5 for the classes & parameters example) |
dtsx produces sound types (correctly widened for mutable containers) while preserving original values via @defaultValue JSDoc — something neither tsc nor oxc does. No as const, no explicit annotations, no isolatedDeclarations flag required.
Measured on Apple M3 Pro (Mac15,6), 11-core CPU (5 performance + 6 efficiency), 18 GB RAM, macOS 27.0.1 (arm64) — 2026-11-12.
dtsx 0.11.52 at ba9df16 · Bun 1.4.4 · Zig 0.17.1 · oxc-transform 0.158.0 · TypeScript 7.0.2 (native tsc) · tsdown 0.23.7.
API and CLI values are the median of 3 full runs; the 1-minute load average ranged 4.5–8.2 during measurement.
Reproduce with bun run benchmark:prepare && bun run benchmark:publish. Raw numbers live in benchmark/results.json.
- Every tool emits real declarations for the same inputs; the harness verifies each tool's output before timing and aborts if any tool fails, so no row can time a crash.
- dtsx runs in its default value-inferring mode (narrow types and
@defaultValueJSDoc), which does more work than isolated-declarations-only emitters. tscis TypeScript 7's native Go compiler, run directly (no Node launcher) with--noCheck, its fastest declaration path. It has no in-process API, so it only appears in the CLI tables.- oxc has no declaration CLI, so its CLI rows run a minimal Bun-compiled wrapper around
oxc-transform(bun build --compile --splitting --bytecode). - "No cache" means every dtsx cache is cleared on every iteration. The warm-cache table is reported separately and never used for comparisons.
- API and CLI per-run means come from mitata, aggregated as described above. Bundled builds use the sample means described below. Lower is better; bold marks the fastest tool per column.
One file transformed in the same process: dtsx through its native binding, oxc-transform through N-API.
| Tool | Small (~50 lines) | Medium (~100 lines) | Large (~330 lines) | XLarge (~1050 lines) |
|---|---|---|---|---|
| dtsx | 6.22 µs | 13.58 µs | 47.00 µs (1.10x) | 189.18 µs |
| oxc-transform | 6.24 µs (1.00x) | 18.55 µs (1.37x) | 42.81 µs | 213.06 µs (1.13x) |
Each tool runs as an executable via subprocess, so process startup is included.
| Tool | Small (~50 lines) | Medium (~100 lines) | Large (~330 lines) | XLarge (~1050 lines) |
|---|---|---|---|---|
| dtsx | 1.65 ms | 1.78 ms | 2.00 ms | 2.21 ms |
| oxc | 9.13 ms (5.5x) | 8.82 ms (5.0x) | 9.33 ms (4.7x) | 9.02 ms (4.1x) |
| tsc 7 | 29.25 ms (18x) | 28.00 ms (16x) | 29.41 ms (15x) | 31.03 ms (14x) |
A whole directory of generated modules. dtsx and tsc use multiple threads; the oxc wrapper processes files sequentially because oxc ships no parallel declaration CLI.
| Tool | 50 files | 100 files | 500 files |
|---|---|---|---|
| dtsx | 8.94 ms | 17.76 ms | 64.10 ms |
| oxc | 23.89 ms (2.7x) | 35.76 ms (2.0x) | 132.45 ms (2.1x) |
| tsc 7 | 56.57 ms (6.3x) | 86.49 ms (4.9x) | 299.49 ms (4.7x) |
The @dtsx/dtsx SDK, which powers the bundler and build-tool plugins, called in-process like any other JavaScript SDK. It ships the native engine for each platform and runs it in-process through Bun FFI, falling back to its TypeScript engine where no native library applies.
| Tool | Small (~50 lines) | Medium (~100 lines) | Large (~330 lines) | XLarge (~1050 lines) |
|---|---|---|---|---|
| oxc-transform | 6.24 µs | 18.55 µs (1.04x) | 42.81 µs | 213.06 µs |
| dtsx JS SDK | 8.82 µs (1.41x) | 17.83 µs | 54.52 µs (1.27x) | 217.17 µs (1.02x) |
Fresh builds of the same project. Each value is the mean of 5 measured samples per tool/project. Strict positive and negative consumer type-checks run outside timing.
| Tool | 2 source files | 11 source files | 101 source files |
|---|---|---|---|
| dtsx JS SDK | 2.72 ms | 5.90 ms | 43.45 ms |
| tsdown JS SDK | 40.53 ms (14.9x) | 41.29 ms (7.0x) | 71.71 ms (1.7x) |
The SDK's content-hash cache returns an unchanged file's declarations without re-scanning. No other tool here caches, so this is for reference only.
| Small (~50 lines) | Medium (~100 lines) | Large (~330 lines) | XLarge (~1050 lines) | |
|---|---|---|---|---|
| dtsx JS SDK (warm cache hit) | 108 ns | 180 ns | 385 ns | 1.40 µs |
| Platform | dtsx | oxc | tsgo | tsc |
|---|---|---|---|---|
| macOS arm64 | 479 KB | 3.7 MB (8x) | 27.7 MB (59x) | 22.5 MB (48x) |
| macOS x64 | 515 KB | 4.0 MB (8x) | 28.6 MB (57x) | 22.5 MB (45x) |
| Linux x64 | 613 KB | 4.6 MB (8x) | 28.1 MB (47x) | 22.5 MB (38x) |
| Linux arm64 | 524 KB | 4.1 MB (8x) | 27.1 MB (53x) | 22.5 MB (44x) |
| Windows x64 | 757 KB | 3.7 MB (5x) | 28.7 MB (39x) | 22.5 MB (30x) |
| FreeBSD x64 | 502 KB | 4.3 MB (9x) | — | 22.5 MB (46x) |
Install the pinned tools and dependencies through Pantry, then build the actual native producer before running its consumers:
pantry install --frozen-lockfile eval "$(pantry env)" (cd packages/zig-dtsx && zig build && zig build test) bun run test
Before publication, build the core and every adapter, then check their normal package archives in strict installed SDK consumers:
bun run build
bun --filter './packages/*-plugin' build
bun run typecheck:public-sdk
bun run test:public-sdkThe SDK gate checks advertised exports, runtime dependency edges, normal/frozen installs, installed payload hashes, strict NodeNext typing, precise invalid consumers and Bun imports. It also checks the optional formatter with its provider present and absent. The command prints an evidence directory containing raw command logs and receipt.json; set DTSX_SDK_EVIDENCE to choose its parent directory. Failed installations stay intact. Passed consumers release their derived node_modules after all checks; archives, locks, probes, logs and payload hashes remain available. Set DTSX_SDK_KEEP_INSTALLS=1 to retain passed installations too. Its owned candidate registry serves the exact archives without changing their manifests. Public registry releases and native artifacts require separate verification. CI retains this evidence and blocks publication when any SDK or advertised host-provider baseline fails.
Set DTSX_SDK_PACKAGES_ROOT to qualify extracted published payloads with the same matrix. That directory must contain dtsx, bun-plugin, vite-plugin, esbuild-plugin, webpack-plugin, tsup-plugin and tsdown-plugin package directories. The gate packs those payloads without building them; authored controls and tool versions still come from this checkout. The receipt records the selected package root. Verify registry integrity and native artifacts separately.
Also set DTSX_SDK_REGISTRY_MODE=public to install the exact released SDK versions, hosts and transitive dependencies from public npm. This mode requires explicit package payload inputs, rejects registry override environment variables, starts no candidate registry, and compares the normally installed SDK files against those inputs. Install commands pin the registry, and the gate validates each locked provider before recording the public graph. It runs the same consumer controls and frozen-lock checks. The receipt distinguishes this public registry proof from the default prepublication provider checks.
The strict declaration-package fixtures also accept published producers. Set DTSX_DECLARATION_PRODUCERS to a JSON file naming the same exact version and the actual package directories:
{
"version": "0.11.46",
"core": "/tmp/dtsx-host/node_modules/@dtsx/dtsx",
"bunPlugin": "/tmp/dtsx-host/node_modules/@dtsx/bun",
"native": "/tmp/verified-zig-dtsx/package"
}Install the core and plugin normally, including a frozen installation, and verify the native package's registry integrity first. The native directory must contain its released prebuilt library and no local zig-out build. Run bun test --tsconfig-override /tmp/dtsx-host/tsconfig.json ./packages/dtsx/test/class-field-constructor-package.test.ts with an alias-free host config (paths: {}). The helper checks every version and the plugin's actual core-module resolutions before loading the public import targets. Published mode rejects DTSX_NATIVE_LIBRARY. It uses the same authored controls, emission modes, archive/install steps and precise consumer diagnostics, and never falls back to source when published inputs fail. DTSX_DECLARATION_PRODUCER_RECEIPT records loaded entries, resolved producer imports and hashes; DTSX_TEST_TRACE records their use in each phase. Source producers remain the default when no configuration is supplied.
Please see our releases page for more information on what has changed recently.
Please review the Contributing Guide for details.
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
For casual chit-chat with others using this package:
Join the Stacks Discord Server
“Software that is free, but hopes for a postcard.” We love receiving postcards from around the world showing where dtsx is being used! We showcase them on our website too.
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎
We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
The MIT License (MIT). Please see LICENSE for more information.
Made with 💙
