Issue:
I have encountered difficulties finding an efficient solution for this. Here's a summary of what I attempted:
- To start, ensure that
allowJs
andcheckJs
are both set totrue
in thetsconfig.json
, then include the project files accordingly.
Type checking works well when writing files within the project (intended for publication as an NPM library): a single JS file with JSDoc comments can be imported into any other JS file seamlessly.
The challenge arises when attempting to publish the project and make the types visible in downstream dependencies. For instance, exposing types in an application that installs the published project as a package using npm install the-package
.
After confirming the project functions correctly locally, attempts were made to publish it. Several methods were explored, considering all source code resides in the project's src/
folder, but each approach encountered obstacles preventing it from functioning as required (with explanations provided for each method regarding why it is unsuitable):
Do not produce any output:
- Set
types
tosrc/index.js
- TypeScript does not recognize types from JSDoc comments in JS files, rendering this method ineffective.
- Set
Generate declaration files exclusively:
Output declarations to a separate directory
- Enable
emitDeclarationOnly
by setting it totrue
- Specify the
outDir
as, for example,types
- This results in only declaration files being produced in the
types
folder - In the
package.json
, definetypes
astypes/index.d.ts
(for instance, ifsrc/index.js
serves as the package entry point exporting everything) - This method functions smoothly when importing from
the-package
; for example, `import {whatever} from 'the-package' - However, unlike traditional TypeScript libraries (which output JS and declarations together), this method is flawed when trying to import sub-paths - TypeScript will fail to locate the types for these files (although VS Code will conveniently execute
Go To Definition
leading you to the correct place, where the JSDoc-commented types exist, yet TypeScript seems to disregard them)
- Enable
Also emit declarations next to source files
- Enable
emitDeclarationOnly
by setting it totrue
- Specify the
outDir
assrc
to have the declarations generated alongside all.js
files - Initially, this operates without issues as any consumer can successfully import items with comprehensive type definitions
- Upon running another TypeScript build, errors may emerge stating `error TS5055: Cannot write file '/the-package/src/something.d.ts' because it would overwrite input file`. Essentially, TypeScript now treats previously written declarations as input files
- Enable
Produce both declaration and .js files
- Output files to
types/
- Although effective, this setup results in duplicate JS files existing in both
types/
andsrc/
, needlessly replicating the source - The source location should remain consistent before and after implementing TypeScript; no alterations are desired in either developers or consumers' source locations. The primary objective of creating JS + JSDoc is to maintain a unified source of JS files that work seamlessly, regardless of whether there are type definitions for TS users
- Although effective, this setup results in duplicate JS files existing in both
- Output files to
Frequently remove sibling .d.ts files before every build
- Output .d.ts files adjacent to src/*.js files, and prior to each build, execute
rm -rf src/**/*.d.ts*
first.- Failure to delete the files will lead to TypeScript generating errors like:
error TS5055: Cannot write file '/the-package/path/to/foo.d.ts' because it would overwrite input file.
- This approach also causes disruptions (e.g., TypeScript server, or other build tools reliant on TS types such as ESLint with TS plugins in watch mode) inevitably break due to source files vanishing and reappearing. It creates a poor developer experience beyond just the build process itself
- Failure to delete the files will lead to TypeScript generating errors like:
- Output .d.ts files adjacent to src/*.js files, and prior to each build, execute
Attempt mapping using wildcards in the
exports
field of thepackage.json
- I experimented with:
"exports": { "./src/*": { "types": "./types/*", "default": "./src/*" } },
- I also tested:
"exports": { "./src/*.js": { "types": "./types/*.d.ts", "default": "./src/*.js" } },
- Unfortunately, no success was achieved. The consumer importing a subpath like
cannot access types for that specific fileimport {...} from 'the-package/src/path/foo.js'
- I experimented with:
Place sibling
.d.ts
files insrc/
and attempt to exclude them as input files intsconfig.json
- Add this to package.json:
"include": ["src"], "exclude": ["src/**/*.d.ts"]
- This strategy fails; with any build subsequent to generating .d.ts files, TypeScript continues giving errors akin to those observed in method 4,
error TS5055: Cannot write file ... because it would overwrite input file
- Add this to package.json:
All available options are unfeasible for various reasons outlined above.
Additional Insights:
Here is the tsconfig used:
{
"compilerOptions": {
"outDir": "./types", // for method 2, change this to "./src"
"allowJs": true,
"checkJs": true,
"strict": false,
"module": "ESNext",
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"jsx": "preserve",
"jsxImportSource": "pota",
"sourceMap": true,
},
"include": ["src"],
"exclude": ["src/**/*.d.ts"] // try to ignore src/*.d.ts files for method 6
}
Below are pertinent snippets from package.json prior to incorporating TypeScript (with segments relating to attempted methods commented out):
{
"type": "module",
"//types": "./types/exports.d.ts",
"//types": "./src/exports.d.ts",
"main": "./src/exports.js",
"//exports": {
"./src/*": {
"types": "./types/*",
"default": "./src/*"
}
},
}
All source files reside in src/**/*.js
. If any declarative files must be generated, they are placed in types/
(with the aim of bypassing new file creation and enabling TS to retrieve types directly from JSDoc comments upon consumer importation of the lib).
To initiate the build, simply execute tsc
(or npx tsc
if TypeScript isn't installed globally).
Query:
How can this be accomplished without superfluous duplication of files from src/
to another directory, minus causing TS build malfunctions (due to sibling .d.ts files in src/), without disrupting tool operations through repeated deletion of sibling .d.ts files during each rebuild, and preferably avoiding the emission of any declaration files if feasible (though emitting declarations is acceptable if necessary)?
Prerequisite: In transitioning the JS project (a package) to TypeScript (using JS+JSDoc), developers working on the package should continue modifying JS files in src/
while consumers must retain the ability to utilize files in src/
(method 3 deemed impractical).