Repository navigation
TypeScript won't import JSDoc types from .js file in node_modules #33136
Description
TypeScript Version: 3.5.2
I use JSDoc type definitions in my JavaScript and use checkjs in VSCode to have TypeScript type check my JavaScript live. This generally works very well. However I have noticed one failure. TypeScript fails to properly understand JSDoc types when they're imported from another file. Let me explain.
I have a node module which uses JSDoc to define types. Some of the types are defined in one file and imported into other files using the format:
// vnode.js
/**
* @typedef {Object.<string, any> | {}} Props
* @property {Children} Props.children
*/
/**
* @typedef {VNode[]} Children
*/
/**
* @typedef {string | number | Function} Type
* @typedef {number | string | null} Key
* @typedef {Object.<string, any>} VNode
* @property {Type} VNode.type
* @property {Props} VNode.props
* @property {Children} VNode.children
* @property {Element} VNode.element
* @property {Key} [VNode.key]
* @property {number} VNode.flag
*/This type VNode gets imported in another file for use as a parameter of a function:
// render.js
/**
* Render a functional component. The first argument is the component to render. This can be either a JSX tag or an `h` function. The second argument is the container to render in. An optional third argument is an element in the document loaded from the server. This will be hydrated with the component provided to render. This third argument can be a DOM referece for that element, or a valid string selector for it.
* @typedef {import('./vnode').VNode} VNode
* @param {VNode} VNode
* @param {Element | string} container
* @param {Element | string} [hydrateThis]
* @return {void} undefined
*/In the module itself this works as expected. The type VNode is understood everywhere it is used.
The problem is when I create a project the uses this module. For some reason TypeScript treats the imported JSDoc type as any. I'm currently using VSCode Version: 1.37.1. When i was using 1.36.x I was getting the following warning for the import JSDoc type (sic):
TypeScript cannot find a `d.ts` file for `./vnode`. As such its types will be treated as `any`.
What I don't understand is why would TypeScript be looking for a d.ts file for a JavaScript file using JSDoc comments for types? Does it know that an import in a JSDoc comment should point to a JSDoc type definition? It seams like when TypeScript sees the @typedef import statement, it assumes its a normal JavaScript import and starts looking for a d.ts file. Which leads to it never importing the JSDoc type.
Of course I could copy and paste the type definition everywhere I need it. However that leads to maintenance problems when I need to update the type definition.
Since this works inside the module importing JSDoc types from other files, why can't it work when the module is imported into another project?
To show the problem here are some images illustrating a module with a render function. In the first image you can see that the imported VNode type is being imported and interpreted correctly:
In the next image you can see that this imported type is being interpreted correctly as a parameter of the render function when this is defined:
But here, when the render function is imported into a project, the imported type is treated by TypeScript as any:
To reproduce this problem do the following.
npx @composi/create-composi-app -n Type-TestThe above will creaate a new project on your desktop named Type-Test. cd to the new project and run:
npm installAfter the dependencies are installed, open the project in VSCode and go to the src/js/app.js file. Hover over the imported render function at the top of the page. You'll see Intellisense pop up, but the first argument for VNode will be of type any. This is wrong.
Now open the project's node_modules folder and go to node_modules/@composi/core/src/render.js Scroll down to the defintion of the render funcion. Hover over it or its first parameter. You'll see that here TypeScript is able to correctly understand the imported type as VNode, not as any.
It seams TypeScript is treated JSDoc import statements as if they were JavaScript imports. It then looks for a .d.ts, which doesn't exist because this is JavaScript with JSDoc comments. So it defaults to any.
Interestingly, in earlier versions of VSCode, if I opened the module source code first and hovered over the imported type, this would force VSCode and TypeScript to recognize the type, even in the project where the modules was imported. Currently this no longer works, instead treating such imported JSDoc types as any no matter what you do.



Activity
rbiggs commentedon Sep 5, 2019
Update
Recently I noticed a situation where TypeScript was able to properly import JSDoc types from an imported module. In the case of my NPM module @composi/core, it has the following file structure:
Of the above,
index.jsexports functions defined in the other files like so:When using @composi/core, a project usually imports functions like this:
@composi/core's
package.jsonhas theindex.jsas the main point of entry:However, for better tree shaking you could just import the functions you want directly from the other files in the source directory, bypassing the
index.jsfile. You would do that like this:When one does this, all of a sudden TypeScript imports the types correctly for
handrenderwithout casting them toany.I'm suspecting that the indirection of the
index.jsfile is what is causing the problem. TypeScript is probably going to theindex.jsfile. There's no JSDoc comments there because its just imports and exports. There is also nod.tsassociated with theindex.jsfile. TypeScript follows the path to the individual files. It sees nod.tsfiles for them. It sees the import of JSDoc types. Then it looks for ad.tsfor them and doesn't find one. So it treats the types asany. Of course this is my hunch. I don't know the details of how TypeScript actually handles this whole process of understanding JavaScript type detection between ordinary untyped JavaScript and JavaScript with JSDoc comments. But I'm suspecting there is something amiss in what decisions it makes as it follows the path of an JavaScript module import, especially concerning modules withd.tsfiles and without.phaux commentedon Sep 6, 2019
There's an option in TS
compilerOptionscalledmaxNodeModuleJsDepthand it's set to some low value by default. Try setting that to a higher value in the client program's tsconfig.rbiggs commentedon Sep 6, 2019
maxNodeModuleJsDepthis for TypeScript. I'm talking about JavaScript files with JSDoc comments. I'm not compiling them since they're already JavaScript. I'm importing them into a JavaScript project that usescheckjsfor live type linting.Now, projects do have a
jsconfig.jsonfile with compiler options, but setting that with, "maxNodeModuleJsDepth": 5or higher doesn't seem to make a difference. Here's myjsconfig.json:rbiggs commentedon Sep 9, 2019
Update
I found a hack for countering TypeScripts inability to properly process imported JSDoc types. I imported the file with the types that other files were using into the module's
index.jsfile. By default I wouldn't do this because all the functions defined invnode.jsare only used internally and should not be exposed to the end user. However, by importing it intoindex.js, its types get exposed to TypeScript so that when it sees them being imported in other files, it parses them correctly.Of course this is a hack that illustrates the problem. A module with an
index.jsfile that assembles functions from disparate files for export causes TypeScript to fail to follow their import of JSDoc types.My old
index.jsfile, as mentioned above was like this:My new version that forces TypeScript to recognize imported JSDoc types is like this:
Note that the
createVNodefunction is imported but not exported. It shouldn't be exposed to the user. It's merely being imported to expose the JSDoc types defined in that file so that TypeScript can understand them when it encounters them in the other functions that are getting exported.rbiggs commentedon Sep 19, 2019
I'm currently using TypeScript 3.6.3. I'm noticing ellipsis before the imported module name for
@composi/core. When I hover over the ellipsis, I get the following notice about not finding ad.tsfile for@composi/core. Please note the@composi/coreis fully typed using JSDoc comments. In fact hover over the individual imports from the module shows the correct types for them.Why should I need a
d.tsfile when the types are already fully defined in JSDoc comments?engineforce commentedon Oct 2, 2019
I have found a solution that seem to work well: move
index.jstosrc/folder, and createindex.d.tsat root that point to it:rbiggs commentedon Oct 2, 2019
I also found another hack to get TypeScript to import JSDoc types that weren't getting imported. Just make so
@typedefimports on theindex.jsfile for the module. Then TypeScript has to parse them at that level and they become available when you import the module in a project.But I'm curious what you're doing with the
d.tsfile. Al it has is the import of the JavaScript functions with the JSDoc types you want to expose? Hmmm.... I'll try that, but I really don't want to have to create ad.tsfile. I'm of the opinion that TypeScript should be able to understand the types of a JavaScript imported module based on its JSDoc type definitions.rbiggs commentedon Oct 2, 2019
I tried using a
d.tslike you suggested to point TypeScript toward the type definitions, but that did not work. Instead, this is what works for me. I have the following expected exports in the module'sindex.jsfile, followed by@typedefpointing to the type definitions in JSDoc that I want TypeScript to be aware of globally:This allows the user to import
h,render, etc. and get the correct types in Visual Studio Code.lukehesluke commentedon Jan 27, 2020
For anyone else who has found this issue after it closed, hopefully this helps:
Same Problem
I have the same problem as Robert Biggs (@rbiggs). I've created a module called
@imin/shared-data-types. This module uses almost entirely JSDoc to define its TypeScript types. Within the module, all the files are able to use the types correctly. VSCode recognises if there's a type error and so doestsc. That's great ✔️However, as soon as I'm importing this code to another project, the types disappear. VSCode and
tscboth recognise the types asany❌Hope?
Niki (@phaux) 's solution (#33136 (comment)) was very helpful to me. I set my tsconfig.json to:
{ "compilerOptions": { "noEmit": true, "allowJs": true, "checkJs": true, "downlevelIteration": true, "maxNodeModuleJsDepth": 1 }, "include": [...] }And VSCode and
tscwere finally able to recognise the types!Before:
After:
Wonderful ✔️
The only problem is that when I ran
tscI was shown a LOT of new errors.This is because now every single import is being type checked. Many JavaScript libraries come a-cropper when examined under TypeScript's inscrutable gaze. Fair enough! But I don't have time to fix all these issues with external libraries. ❌
What I want is for TypeScript to just check the modules that I made that I know have been typed properly using JSDoc. How can I do that?
The solution
Just add the modules to
tsconfig.json'sincludelist. e.g.:This works! ✔️ ✔️ ✔️
Typescript version: 3.6.2
[-]TypeScript fails to import JSDoc type from module[/-][+]TypeScript won't import JSDoc types from .js file in node_modules[/+]rbiggs commentedon Jan 28, 2020
Actually, Luke Winship (@lukehesluke), that's what I wound up doing as well, using
includeto tell TypeScript to include my JavaScript modules with JSDoc comments. One thing I also had to do what make sure the main index file imported any types being used in sub-files so that when TypeScript hit the index.js file for the module, it also found the path to the types.However, now I no longer bother with any of that because TypeScript now supports creating
d.tsfiles from JavaScript files with JSDoc comments. So when I runtsc, it automatically generates.d.tsfiles for everything. This solves the problems that TypeScript still has with following the paths of types defined with JSDoc in complex JavaScript modules. Well, it doesn't fix the issue. It gets around the problem because by default TypeScript always looks ford.tsfiles. Since my modules are written in JavaScript with JSDoc comments, there weren't any. Now I can let TypeScript create them for me while its also type checking my JavaScript.You just need to update your projects
tsconfig.jsonfile for this to work. Here's what I'm currently using for compiler options:{ "compilerOptions": { "target": "es6", "allowJs": true, "checkJs": true, "moduleResolution": "node", "alwaysStrict": true, "strictNullChecks": false, "emitDeclarationOnly": true, "declaration": true, "outDir": "types", "removeComments": false } }Note that I tell TypeScript to output the
d.tsfiles into a folder called types. Then in mypackage.jsonfile I declare that as the location for the project's types:With these updates to my JavaScript projects, TypeScript will find the
d.tsfiles for my JavaScript projects. And these are created by TypeScript based on my JSDoc comments. Win win.I guess I should mention how I check my types and produce the
d.tsfiles. Because I have all the instructions for TypeScript in the project'stsconfig.jsonfile, all I have to do is run a simple npm script which I define in mypackage.json:Then in the terminal I just run
npm run tsc. This runs a type check on the JavaScript using my JSDoc comments and then producesd.tsfiles as well.lukehesluke commentedon Jan 31, 2020
Robert Biggs (@rbiggs) you genius! Thank you, that looks like the ideal solution to this. Am I right in saying that this is primarily these compiler options:
rbiggs commentedon Jan 31, 2020
Yup. You do need the latest TypeScript installed somewhere, either locally or globally. Latest version is 3.7.5. Set up your
tsconfig.jsonfile. Then setup an NPM script to run TypeScript to check you files. With this above options in thetsconfigfile it will also create thed.tsfiles for your JavaScript. That means you don't have to worry about TypeScript importing you JSDoc types properly. It will always find and parsed.tsfiles. Here's a link to atsconfigfile for one of my Gitub repos: https://github.com/composi/core/blob/master/tsconfig.jsonsamuelstroschein commentedon Oct 16, 2023
This issue should be re-opened given that TypeScript does not resolve JS files with JSDoc from node_modules.
All proposed solutions in this issue are workarounds. Using the TypeScript compiler to emit declaration files is redundant, given that the JS files contain the declaration already. Moreover, I experienced:
All of those issues would exist if declaration files wouldn't be required for JSDoc JS files.
phaux commentedon Oct 18, 2023
I think TypeScript should simply support
mainandtypesfields inpackage.jsonbeing set to the same file if the file is JS with JSDoc:{ "main": "./main.js", "types": "./main.js" }Last time I checked it didn't work. Project which imports such module doesn't get the types.
kungfooman commentedon Jan 19, 2024
Fast forward to 2024 and it still doesn't work and issue is still closed... nothing is fixed here, just hacky and annoying workarounds in sight.
ilyaigpetrov commentedon Jan 20, 2024
Please, reopen the issue. Thumbs up this message if you want it to be reopened. Thanks.
hyfdev commentedon May 17, 2024
Happy to shared that I have found a way to do similar things and its more friendly due to not need to config
includeormaxNodeModuleJsDepth.In short, you just need to create a facade
.d.tsfile for each entry that you want to export andexport *from these entries in.d.tsfiles. Then, add.d.tstopackage.json#exportsfield. It will enforce typescript to read types on js + jsdoc files.https://github.com/hyf0/starter-libesm is an example.
.d.tsfile: https://github.com/hyf0/starter-libesm/blob/4509f5db37de5cfa53054c53124b42b21ff20087/index.d.tsexport *from the entry: https://github.com/hyf0/starter-libesm/blob/4509f5db37de5cfa53054c53124b42b21ff20087/index.d.ts#L1.d.tstopackage.json#exports: https://github.com/hyf0/starter-libesm/blob/4509f5db37de5cfa53054c53124b42b21ff20087/package.json#L10Too bad you still need to enable
package.json#checkJsin the project to enable type checking on.jsfiles innode_modules.