Closed rbiggs closed 4 years ago
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:
+ core
+ src
--constants.js
--effects.js
--fragment.js
--h.js
--index.js
--render.js
--runtime.js
--union.js
--vdom.js
--vnode.js
Of the above, index.js
exports functions defined in the other files like so:
export { h } from './h'
export { render } from './render'
export { run } from './runtime'
export { union } from './union'
export { batchEffects } from './effects'
export { Fragment } from './fragment'
When using @composi/core, a project usually imports functions like this:
import { h, render, run, union, batchEffects } from '@composi/core'
@composi/core's package.json
has the index.js
as the main point of entry:
"main": "src/index.js"
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.js
file. You would do that like this:
import {h} from '@composi/core/src/h'
import {render} from '@composi/core/src/render'
When one does this, all of a sudden TypeScript imports the types correctly for h
and render
without casting them to any
.
I'm suspecting that the indirection of the index.js
file is what is causing the problem. TypeScript is probably going to the index.js
file. There's no JSDoc comments there because its just imports and exports. There is also no d.ts
associated with the index.js
file. TypeScript follows the path to the individual files. It sees no d.ts
files for them. It sees the import of JSDoc types. Then it looks for a d.ts
for them and doesn't find one. So it treats the types as any
. 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 with d.ts
files and without.
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 in the client program's tsconfig.
maxNodeModuleJsDepth
is 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 uses checkjs
for live type linting.
Now, projects do have a jsconfig.json
file with compiler options, but setting that with , "maxNodeModuleJsDepth": 5
or higher doesn't seem to make a difference. Here's my jsconfig.json
:
{
"compilerOptions": {
"target": "es6",
"jsx": "react",
"maxNodeModuleJsDepth": 5
},
"include": [
"node_modules/@composi/**/src/*",
]
}
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.js
file. By default I wouldn't do this because all the functions defined in vnode.js
are only used internally and should not be exposed to the end user. However, by importing it into index.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.js
file that assembles functions from disparate files for export causes TypeScript to fail to follow their import of JSDoc types.
My old index.js
file, as mentioned above was like this:
export { h } from './h'
export { render } from './render'
export { run } from './runtime'
export { union } from './union'
export { batchEffects } from './effects'
export { Fragment } from './fragment'
My new version that forces TypeScript to recognize imported JSDoc types is like this:
export { h } from './h'
export { render } from './render'
export { run } from './runtime'
export { union } from './union'
export { batchEffects } from './effects'
export { Fragment } from './fragment'
/**
* The following import is a hack to force TypeScript to properly
* understand JSDoc types defined in the vnode.js file that are used by
* fragment.js, h.js, render.js and vdom.js.
* When TypeScript gets updated to handle this import properly, this will be removed.
*/
import { createVNode } from './vnode' // eslint-disable-line no-unused-vars
Note that the createVNode
function 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.
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 a d.ts
file for @composi/core
. Please note the @composi/core
is 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.ts
file when the types are already fully defined in JSDoc comments?
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 }
I also found another hack to get TypeScript to import JSDoc types that weren't getting imported. Just make so @typedef
imports on the index.js
file 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.ts
file. 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 a d.ts
file. 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.
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.
For anyone else who has found this issue after it closed, hopefully this helps:
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 :heavy_check_mark:
However, as soon as I'm importing this code to another project, the types disappear. VSCode and tsc
both recognise the types as any
:x:
@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:
After:
Wonderful :heavy_check_mark:
The only problem is that when I ran tsc
I 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. :x:
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?
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! :heavy_check_mark: :heavy_check_mark: :heavy_check_mark:
Typescript version: 3.6.2
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.
@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:
"emitDeclarationOnly": true,
"declaration": true,
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
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.
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.
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.
Please, reopen the issue. Thumbs up this message if you want it to be reopened. Thanks.
I think TypeScript should simply support
main
andtypes
fields inpackage.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.
Happy to shared that I have found a way to do similar things and its more friendly due to not need to config include
or maxNodeModuleJsDepth
.
In short, you just need to create a facade .d.ts
file for each entry that you want to export and export *
from these entries in .d.ts
files. Then, add .d.ts
to package.json#exports
field. It will enforce typescript to read types on js + jsdoc files.
https://github.com/hyf0/starter-libesm is an example.
.d.ts
file: 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.ts
to package.json#exports
: https://github.com/hyf0/starter-libesm/blob/4509f5db37de5cfa53054c53124b42b21ff20087/package.json#L10Too bad you still need to enable package.json#checkJs
in the project to enable type checking on .js
files 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:
This type
VNode
gets imported in another file for use as a parameter of a function: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):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 ad.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 importedVNode
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 asany
:To reproduce this problem do the following.
The above will creaate a new project on your desktop named
Type-Test
.cd
to the new project and run:After the dependencies are installed, open the project in VSCode and go to the
src/js/app.js
file. Hover over the importedrender
function at the top of the page. You'll see Intellisense pop up, but the first argument for VNode will be of typeany
. This is wrong.Now open the project's
node_modules
folder and go tonode_modules/@composi/core/src/render.js
Scroll down to the defintion of therender
funcion. Hover over it or its first parameter. You'll see that here TypeScript is able to correctly understand the imported type asVNode
, not asany
.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 toany
.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.