How can I export package-scope JSDoc types?

2k Views Asked by At

I'm looking for a workaround for this bug in tsc: https://github.com/microsoft/TypeScript/issues/50436

Problem 1:

Package-scoped types are not available outside of a package.

  1. If you declare a type in ./types.js (and include that file in jsconfig.json.include then that type is scoped to your package - any file can use it. Example:
    // types.js
    /**
     * @typedef {Object} Foo
     * @prop {String} name
     */
    
    // index.js
    /** @type {Foo} */
    let foo;
    
  2. You cannot import that type from another package. Example:
    // bar.js
    /** @type {import('foo/types.js').Foo} */
    let foo;
    
    // ❌ [tsserver] Cannot find name 'Foo'.
    

Problem 2

Exported types are not available in the package-scope.

  1. If you add an export to ./types.js, the types are no longer available at the package level. Example:
    // types.js
    module.exports = {}; // causes types to be exported
    
    /**
     * @typedef {Object} Foo
     * @prop {String} name
     */
    
    // index.js
    /** @type {Foo} */
    let foo;
    
    // ❌ [tsserver] Cannot find name 'Foo'.
    
  2. But now import that type from another package. Example:
    // bar.js
    /** @type {import('foo/types.js').Foo} */
    let foo;
    

Reduced Test Case Git Repo

Here's a reduced test case with all the tsconfig.json, package.json, etc:

https://github.com/coolaj86/test-case-tsc-exports

.
├── README.md
├── main.js
├── tsconfig.json
└── node_modules
    ├── bar
    │   ├── bar.js
    │   ├── package.json
    │   ├── tsconfig.json
    │   └── types.js
    └── foo
        ├── foo.js
        ├── package.json
        ├── tsconfig.json
        └── types.js

Catch 22

How can I both export types and have them available in the package scope?

1

There are 1 best solutions below

0
lepsch On

The thing is that foo/types.js is declaring a global type only accessible in the package it's declared in. This is because foo/types.js is not a module so everything becomes global inside it. From the docs:

Files are only considered modules when TypeScript finds an import or an export. In some basic cases, you might need to write out export {} as some boilerplate to make sure of this.

To fix the problem then both foo/types.js and bar/types.js should have an export like module.exports = {}; (or the one you've written in bar/types.js or even ES modules like export {}).

The following are the types.js files:

bar/types.js

"use strict";

/**
 * @typedef {Object} Bar
 * @property {Number} age
 * @property {2 | 7 | 11 | 37 | 42} lucky_number
 */

module.exports = {};

foo/types.js

"use strict";

/**
 * @typedef {Object} Foo
 * @property {String} name
 * @property {"vanilla"|"chocolate"|"strawberry"} flavor
 */

 module.exports = {};

And to use the type in the package level it must be the same way it is in Typescript, just importing it. This way everything should work as expected.

bar/bar.js

"use strict";

/** @type {import('./types.js').Bar} */
let bar = {
    age: 37,
    lucky_number: 37,
};

console.log(bar);

foo/foo.js

"use strict";

/** @type {import('./types.js').Foo}*/
let foo = {
    name: "JS",
    flavor: "vanilla",
};

console.log(foo);

main.js

"use strict";

/**
 * @param {import('foo/types.js').Foo} foo
 * @param {import('bar/types.js').Bar} bar
 */
function bazzer(foo, bar) {
    return {
        baz: foo.name,
        qux: foo.flavor,
        quux: bar.age,
        grault: bar.lucky_number,
    };
}

module.exports.bazzer = bazzer;

Here's the repo with the fixes inplace: https://github.com/lepsch/test-case-tsc-exports