DTS-Buddy fixes Type-Declarations
dts-buddy
is one of those tools that’s hard to justify without encountering the problem they solve first. Here’s the story of how I ran into it.
If you don’t care why, you can skip past the intro
A horror story about types in subpackages
I’ve been finding myself copying a lot of code between projects. To make this easier, I’ve been building a package where I keep all my commonly used code. Since it includes code from many domains, using subpackages seemed quite natural. @sigrist.dev/framework/pdf
for all my PDF-related code, @sigrist.dev/framework/ui
for all my UI-related code, and so on.
To keep editing convenient, I opted to use JSDoc types, and generate type-declarations from them.
While doing this, I quickly learned about the pitfalls of using subpackages. Using the TypeScript compiler means I was generating one d.ts
file per js
file. This caused a problems when importing a subpackage. Whenever I started typing import {
the IDE would show me a list of all the types present in the package, including ones that were not meant to be public. This was very irritating.
Another issue I ran into is that go to definition
didn’t work. I couldn’t jump to the implementation of a type, because the IDE didn’t know how to map the type-declaration to the actual source code. As the declarations were always colocated with the JS file it wasn’t that bad, but still inconveniet. I often have to glance at the implementation to recall what was going on, since the package isn’t documented well.
Dts-Buddy; The savior
dts-buddy
solves all these things in a very clever way. Instead of colocating each type-declaration with the JS file it belongs to, it generates just one .d.ts
file for the entire package. This is then referenced by the types
field in your package.json
. The file contains module declarations for the public interface of the package and it’s subpackages, using the declare module
syntax.
Here’s an example output it generated for one of my (private, sorry) packages:
// @filename: dist/types.d.ts
declare module '@sigrist.dev/framework/pdf' {
/**
* Adds a QR-ESR Invoice footer to the given PDF.
* Assumes the current page has A4 portrait format.
*
* @see https://www.swiss-qr-invoice.org/validator/?lang=de for a validator
*/
export function function addQrEsrFooter(pdf: import("jspdf").jsPDF, data: ESRData): import("jspdf").jsPDF
Adds a QR-ESR Invoice footer to the given PDF.
Assumes the current page has A4 portrait format.addQrEsrFooter(pdf: jsPDF
pdf: import("jspdf").class jsPDF
jsPDF, data: ESRData
data: type ESRData = {
amount: number;
reference: string;
}
ESRData) : import("jspdf").class jsPDF
jsPDF;
export type type ESRData = {
amount: number;
reference: string;
}
ESRData = {
amount: number
amount: number;
reference: string
reference: string;
//...
}
// ...
}
declare module '@sigrist.dev/framework/ui' {
// ...
}
Alongside this, it also generates a .map.d.ts
file, which maps the public types onto the actual source code. This allows the IDE to “go to definition” and “peek definition” reliably.
How to use it
First install it:
pnpm i -D dts-buddy
Then create a build.js
file in your project, and use it as your build script:
{
"scripts": {
"build": "node build.js"
}
}
// @filename: build.js
import { createBundle } from 'dts-buddy';
//Generate a bundle of all type-declarations
await createBundle({
project: 'tsconfig.json', //Your tsconfig.json
//Map subpackages to their entrypoints
modules: {
'@sigrist.dev/framework/pdf': 'src/pdf/index.js',
'@sigrist.dev/framework/ui': 'src/ui/index.js'
},
include: ['src'],
output: 'types/index.d.ts' //The resulting type-declaration file
});
The only thing left to do is to tell the module-resolution to actually use the generated file. So, in your package.json
, add a types
field, and also register it in each exports
field.
{
"types": "types/index.d.ts", //here
"exports": {
".": {
"types": "types/index.d.ts", //here
"import": "./src/index.js"
},
"./pdf": {
"types": "types/index.d.ts", //here
"import": "./src/pdf/index.js"
},
"./ui": {
"types": "types/index.d.ts", //here
"import": "./src/ui/index.js"
}
}
}
That’s it! Now you can run npm run build
and it’ll generate a single type-declaration file (+map) for your entire package.
Should you use it?
dts-buddy
is a tool that solves the subpackage-problem very very well. Outside of that, the regular TypeScript compiler is good enough. It’s going to be more familiar to most developers and is maintained more actively. But when you do need dts-buddy
, it’s a lifesaver.
I for one have really enjoyed it and am very likely to choose it again.