TypeScript: TypeScript won't import JSDoc types from .js file in node_modules

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:

1-render-source

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:

2-render-source

But here, when the render function is imported into a project, the imported type is treated by TypeScript as any:

3-render-imported

To reproduce this problem do the following.

npx @composi/create-composi-app -n Type-Test

The above will creaate a new project on your desktop named Type-Test. cd to the new project and run:

npm install

After 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.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 16
  • Comments: 16

Most upvoted comments

Please, reopen the issue. Thumbs up this message if you want it to be reopened. Thanks.

For anyone else who has found this issue after it closed, hopefully this helps:

Same Problem

I have the same problem as @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 does tsc. That’s great ✔️

However, as soon as I’m importing this code to another project, the types disappear. VSCode and tsc both recognise the types as any

Hope?

@phaux 's solution (https://github.com/microsoft/TypeScript/issues/33136#issuecomment-528657072) 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 tsc were finally able to recognise the types!

Before:

Screenshot from 2020-01-27 10-56-17

After:

Screenshot from 2020-01-27 10-58-01

Wonderful ✔️

The only problem is that when I ran tsc I was shown a LOT of new errors.

Screenshot from 2020-01-27 11-01-45

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’s include list. e.g.:

{
  "compilerOptions": {
    "noEmit": true,
    "allowJs": true,
    "checkJs": true,
    "downlevelIteration": true
  },
  "include": [
    "src/**/*.js",
    "node_modules/@imin/shared-data-types/**/*.js",
  ]
}

This works! ✔️ ✔️ ✔️


Typescript version: 3.6.2

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:

  • jump to source requires .d.ts.map files (bloats the package)
  • the declarations don’t contain all comments from the JS file

All of those issues would exist if declaration files wouldn’t be required for JSDoc JS files.

There’s an option in TS compilerOptions called maxNodeModuleJsDepth and it’s set to some low value by default. Try setting that to a higher value.

I think TypeScript should simply support main and types fields in package.json being 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.

Yup. You do need the latest TypeScript installed somewhere, either locally or globally. Latest version is 3.7.5. Set up your tsconfig.json file. Then setup an NPM script to run TypeScript to check you files. With this above options in the tsconfig file it will also create the d.ts files for your JavaScript. That means you don’t have to worry about TypeScript importing you JSDoc types properly. It will always find and parse d.ts files. Here’s a link to a tsconfig file for one of my Gitub repos: https://github.com/composi/core/blob/master/tsconfig.json

Actually, @lukehesluke, that’s what I wound up doing as well, using include to 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.ts files from JavaScript files with JSDoc comments. So when I run tsc, it automatically generates .d.ts files 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 for d.ts files. 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.json file 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.ts files into a folder called types. Then in my package.json file I declare that as the location for the project’s types:

"typings": "types"

With these updates to my JavaScript projects, TypeScript will find the d.ts files 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.ts files. Because I have all the instructions for TypeScript in the project’s tsconfig.json file, all I have to do is run a simple npm script which I define in my package.json:

"scripts": {
  "checkjs": "tsc"
}

Then in the terminal I just run npm run tsc. This runs a type check on the JavaScript using my JSDoc comments and then produces d.ts files as well.

Last time I checked it didn’t work. Project which imports such module doesn’t get the types.

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.

I tried using a d.ts like 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’s index.js file, followed by @typedef pointing to the type definitions in JSDoc that I want TypeScript to be aware of globally:

export { h } from './h'
export { render } from './render'
export { run } from './runtime'
export { union } from './union'
export { batchEffects } from './effects'
export { Fragment } from './fragment'

/**
 * Make types available to programs that use them.
 */
/**
 * Type of virutal node used to define elements to create.
 * @typedef { import('./vnode').VNode } VNode
 */
/**
 * A program which is executed by the `run` function.
 * @typedef { import('./runtime').Program } Program
 */
/**
 * Message dispatched by the `send` function to the program's `update` method.
 * @typedef { import('./runtime').Message } Message
 */
/**
 * Type of state, which can be of any type.
 * @typedef { import('./runtime').State } State
 */
/**
 * Function for sending messages to the program's `update` method.
 * @typedef { import('./runtime').Send } Send
 */

This allows the user to import h, render, etc. and get the correct types in Visual Studio Code.

I have found a solution that seem to work well: move index.js to src/ folder, and create index.d.ts at root that point to it:

// src/index.js
export function delay() {}
// index.d.ts
import { delay } from './src'
export { delay }