Notes on making TypeScript declaration files
Notes on making TypeScript declaration files, a.k.a the .d.ts
files for an npm package.
The case covered assumes multiple js
files, each requiring a .d.ts
.
General idea
Adding type declarations to an existing npm package can bring many benefits such as having text editors showing the function signature or tsc
enforcing type check.
For these reasons and more, you may consider adding TypeScript declaration files, a.k.a the .d.ts
file to the package.
The case
We’ll consider now a package with multiple js files, each accessible independently.
Assuming you have a similar situation:
.
└── src
├── folder
│ └── file2.js
└── file1.js
You have two options for having TypeScript declaration files:
- having the declaration files within the same folder of the js files
- or, have the types in another folder (
Folder redirects
)
Option 1: Types in the same folder
This method consists of having the types declaration files within the same folder of the js files.
No extra information in package.json
required.
For each js
file, create a .d.ts
file.
// Example of types in the same folder
.
├── folder
│ ├── file2.d.ts
│ └── file2.js
├── file1.d.ts
└── file1.js
tsc filename mapping:
While skimming the code, I found in compiler/moduleSpecifiers.ts
a comment that mentions some cases tsc
cover to resolve the declaration files.
// Filename | Relative Module Specifier Candidates | Path Mapping | Filename Result | Module Specifier Results
// --------------------<----------------------------------------------<------------------------------<-------------------||----------------------------
// dist/haha.d.ts <- dist/haha, dist/haha.js <- "@app/*": ["./dist/*.d.ts"] <- @app/haha || (none)
// dist/haha.d.ts <- dist/haha, dist/haha.js <- "@app/*": ["./dist/*"] <- (none) || @app/haha, @app/haha.js
// dist/foo/index.d.ts <- dist/foo, dist/foo/index, dist/foo/index.js <- "@app/*": ["./dist/*.d.ts"] <- @app/foo/index || (none)
// dist/foo/index.d.ts <- dist/foo, dist/foo/index, dist/foo/index.js <- "@app/*": ["./dist/*"] <- (none) || @app/foo, @app/foo/index, @app/foo/index.js
// dist/wow.js.js <- dist/wow.js, dist/wow.js.js <- "@app/*": ["./dist/*.js"] <- @app/wow.js || @app/wow, @app/wow.js
Note: with this method, you can not provide different type declarations for different ts
versions.
If you need to do so, use typesVersions
.
Option 2: Types in a different folder
This method consists of having a separate folder for the types declaration files.
You can have a folder for each ts
version you want to support.
Once created the folder which will contain the types declarations, you can mirror the js folder structure.
.
├── types-folder
│ ├── folder
│ │ └── file2.d.ts
│ └── file1.d.ts
├── folder
│ └── file2.js
└── file1.js
In this case, types-folder
mirrors the current package folder structure.
You’ll need the typesVersions
property within package.json
.
Specify the typesVersions property in package.json
Note: the main purpose of typesVersions
is to allow different types for different ts versions.
This is technically called Folder redirects
.
"typesVersions": {
">=3.1": { "*": ["types-folder/*"] }
},
Explanation
">=3.1" := typescript version
"*": ["types-folder/*"]
^ ^
| |
|
For each file within this path: map it to the folder
Automatically generate types for your js files
Especially if you have many files to cover, having some automatic tool can be handy.
tsc
can generate some scaffolding for you using the emitDeclarationOnly
flag.
# generate the TypeScript declaration files for all the js files and place them in ts-types
tsc ./**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types-folder
--declaration
: generate .d.ts files--allowJs
: allow javascript files to be imported--emitDeclarationOnly
: only emit (generate) .d.ts files; do not emit .js files.
Debug how TypeScript resolves types
In case tsc
is not picking up the types you can try traceResolution
for debugging.
tsc --noEmit --traceResolution
--noEmit
: don’t produce any output--traceResolution
: show how ts resolve imports
Function overload
While writing a function signature in TypeScript
, you may realise you need several signatures to cover the function written in js.
This is happening because as javascript is not a typed language, it may allow a succinct syntax that covers many types.
Depending on the case, you may need to use a function overload
.
Function overload
may be an edge case if you always working only on TypeScript, thus for reference here an example.
// Function overload example with export
declare function fn(x: HTMLDivElement): string;
declare function fn(x: HTMLElement): number;
declare function fn(x: unknown): unknown;
export default fn;
Summary
We saw two possibles strategies to add type declaration files to an existing npm package which are: 1) add types declaration within the same js folders (for each js
file we have a .d.ts
file) and 2) add types declaration within a different folder (in this case, we mirror the js folder within the types folder).
Then, we saw how to automatically generate declaration types from js
files using tsc
with the emitDeclarationOnly
flag and how to debug the type import with the traceResolution
flag.
With this mental model, you should be able to generate, add and debug type declaration files to an existing npm package.
While with the links provided, you should have enough reference points to the code and the documentation to explore the subject independently.