stacks/dtsx
publicClone
Push over the same URL. A password will not work: create a token under access tokens and use it in place of one.
- .claude
- .config
- .github
- .vscode
- benchmark
- docs
- packages
- scripts
- .editorconfig 147 B
- .gitattributes 38 B
- .gitignore 395 B
- benchmark-summary.md 6.4 KB
- bun.lock 85.8 KB
- bunfig.toml 119 B
- CHANGELOG.md 174.0 KB
- CLAUDE.md 1.5 KB
- CONTRIBUTING.md 8.0 KB
- deps.yaml 32 B
- dts.config.ts 313 B
- dtsx 39 B
- LICENSE.md 1.1 KB
- package.json 2.5 KB
- pantry.lock 666 B
- README.md 17.6 KB
- tsconfig.json 1.4 KB

dtsx
Extremely fast, smart
.d.tsgeneration with sound type inference &@defaultValuepreservation.
Features
- 🎯 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
[!NOTE] dtsx works out of the box without
isolatedDeclarations— it infers narrow types directly from your source values. That said, enablingisolatedDeclarationsis 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@defaultValuedocumentation.
Install
bun install -d @stacksjs/dtsx```bash brew install dtsx # wip pkgx install dtsx # wip ``` -->@npmjs.com, please allow us to use the
dtsxpackage name 🙏
Usage
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.
Library
import type { DtsGenerationOptions } from '@stacksjs/dtsx'
import { generate, processSource } from '@stacksjs/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;STX Components
.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/'],
}You may also run:
./dtsx generate
# if the package is installed, you can also run
# bunx dtsx generateCLI
Generate Command
Generate declaration files using the default options:
dtsx generateOr 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 --versionWatch Command
Watch for changes and regenerate automatically:
# Watch with default options
dtsx watch
# Watch specific directory
dtsx watch --root src --outdir dist/typesStdin Command
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.tsOptions
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 (comma-separated, 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')--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.
Type Inference
dtsx vs tsc vs oxc
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.
Why @defaultValue
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 Constants
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 = trueport | debug | |
|---|---|---|
| dtsx | 3000 | true |
| tsc | 3000 | true |
| oxc | 3e3 (mangled!) | boolean |
Object Properties — @defaultValue Preservation
// 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
};Generic Type Replacement
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 |
Deep as const
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 constdtsx 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']
};Promise & Complex Types
export const promiseVal = Promise.resolve(42)| Tool | Output |
|---|---|
| dtsx | Promise<42> (resolved values are immutable) |
| tsc | Promise<number> |
| oxc | unknown (error — requires explicit annotation) |
Full Comparison
| 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) |
| Value info preserved? | Yes | No | No |
| Errors | 0 | 0 | 3 |
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.
Benchmarks
Benchmarked on Apple M3 Pro, macOS (bun 1.3.11, arm64-darwin). Run bun benchmark/index.ts to reproduce.
In-Process API — No Cache
Raw single-transform comparison (cache cleared every iteration).
| Tool | Small (~50 lines) | Medium (~100 lines) | Large (~330 lines) | XLarge (~1050 lines) |
|---|---|---|---|---|
| zig-dtsx | 3.37 µs | 7.05 µs | 21.89 µs | 144.89 µs |
| oxc-transform | 7.36 µs (2.2x) | 21.91 µs (3.1x) | 89.66 µs (4.1x) | 560.86 µs (3.9x) |
| dtsx | 15.52 µs (4.6x) | 34.06 µs (4.8x) | 81.96 µs (3.7x) | 573.92 µs (4.0x) |
| tsc | 169.69 µs (50.4x) | 410.31 µs (58.2x) | 1.03 ms (47.1x) | 4.02 ms (27.7x) |
In-Process API — Cached
Smart caching (hash check + cache hit) for watch mode, incremental builds, and CI.
| Tool | Small (~50 lines) | Medium (~100 lines) | Large (~330 lines) | XLarge (~1050 lines) |
|---|---|---|---|---|
| dtsx | 97.81 ns | 162.55 ns | 376.39 ns | 1.43 µs |
| zig-dtsx | 3.43 µs (35.0x) | 7.16 µs (44.0x) | 22.00 µs (58.5x) | 147.21 µs (103.0x) |
| oxc-transform | 7.35 µs (75.1x) | 22.66 µs (139.4x) | 85.77 µs (227.9x) | 558.72 µs (390.7x) |
| tsc | 236.82 µs (2421x) | 463.06 µs (2849x) | 1.53 ms (4065x) | 4.66 ms (3259x) |
CLI — Single File
Compiled native binaries via subprocess.
| Tool | Small (~50 lines) | Medium (~100 lines) | Large (~330 lines) | XLarge (~1050 lines) |
|---|---|---|---|---|
| zig-dtsx | 2.69 ms | 2.35 ms | 2.28 ms | 3.14 ms |
| oxc | 17.08 ms (6.3x) | 17.12 ms (7.3x) | 17.95 ms (7.9x) | 17.69 ms (5.6x) |
| tsgo | 40.53 ms (15.1x) | 44.10 ms (18.8x) | 44.39 ms (19.5x) | 57.77 ms (18.4x) |
| tsc | 384.25 ms (142.8x) | 407.51 ms (173.4x) | 418.81 ms (183.7x) | 454.74 ms (144.8x) |
Multi-File Project
| Tool | 50 files | 100 files | 500 files |
|---|---|---|---|
| zig-dtsx | 18.10 ms | 31.46 ms | ~140 ms |
| oxc | 48.27 ms (2.7x) | 79.00 ms (2.5x) | ~365 ms (2.6x) |
| tsgo | 244.68 ms (13.5x) | 419.65 ms (13.3x) | - |
| tsc | 871.48 ms (48.1x) | - | - |
Binary Size
| 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) |
Testing
bun testChangelog
Please see our releases page for more information on what has changed recently.
Contributing
Please review the Contributing Guide for details.
Community
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
Postcardware
“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 🌎
Sponsors
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.
Credits
License
The MIT License (MIT). Please see LICENSE for more information.
Made with 💙
[codecov-href]: https://codecov.io/gh/stacksjs/dtsx -->