microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.64k stars 12.44k forks source link

JSDoc can't reference global-injected types declared in an ES module #43894

Open Paril opened 3 years ago

Paril commented 3 years ago

Bug Report

When exporting types from ES6 modules as namespaces in a typings file, some extraordinary care is required in order to make the types "work" properly via JSDoc. It's non-trivial and a bit verbose.

🔎 Search Terms

jsdoc es6 export import namespace

🕗 Version & Regression Information

⏯ Playground Link

Playground unavailable as it spans multiple files.

💻 Code

// ExportedTypes.js
class ClassOne
{
    test() { return false; }
    static test2() { return true; }
}

export { ClassOne }```

```ts
// globals.d.ts
import * as TypesToExport from './ExportedTypes'

declare global
{
    var Types: typeof TypesToExport
}```

``js
// UseTypes.js
/** @type {Types.ClassOne} */
let x = null;
x = new Types.ClassOne();
x.test();
x;

/** @type {typeof Types.ClassOne} */
let y = null;
y = new Types.ClassOne();
y.test();
y;

/** @type {InstanceType<typeof Types.ClassOne>} */
let z = null;
z = new Types.ClassOne();
z.test();
z;

🙁 Actual behavior

Attempting to document the types exported here with JSDoc is confusing. The first thing people would attempt is @type {Types.ClassOne} - this does not work, however, and just puts it in as an "any". Hovering over the namespace or type name gives "any". As a second resort, people might try @type {typeof Types.ClassOne}, but this won't work - note that x.test() is not typed properly. This is typing it as the type of the class, not the class itself. The only way I've been able to make this work properly is via /* @type {InstanceType} /, but that's extremely verbose.

🙂 Expected behavior

The first @type hint should work, as hovering over Types.ClassOne in the context of new shows the correct type, but in JSDoc it does not recognize the namespace or type.

sandersn commented 2 years ago

In Javascript, a value v should be resolved as typeof v if there's no type. And classes have a type, so should resolve normally.

This bug shows that it doesn't work when a class is nested in a value. That should work, but may be complicated to implement.

Edit: There may also be a problem with using a Typescript file as an intermediate. Typescript declarations have stricter rules than Javascript ones.

sandersn commented 2 years ago

The rule for Javascript namespaces is different than I thought. The rule is:

A namespace in Javascript is either a Typescript namespace (which includes enums, go figure) or a symbol that has both a type and a value. I thought that Javascript added any symbol with a value. I'm going to look at the history of this code, then I'll try changing it to the simpler rule and see what breaks.

Later: It hasn't changed since I first wrote it 3 1/2 years ago. There's no commentary on the PR either.

sandersn commented 2 years ago

Oh, right, the problem is that values don't have exports, so you have to do a lot of work in the checker to fake them.

Edit: getPropertyTypeForIndexType mostly does what we want here, but of course get the declaredtype of its referent, not that actual type.

sisou commented 1 month ago

This issue came up for us only with TS v5.5. Before (e.g. with v5.4) it did not occur.

Using @type {InstanceType<typeof MyNamespace.MyType>} works, but is horrible to inject everywhere. @type {MyNamespace.MyType} worked until v5.5.

Context

In our case the package has a types.d.ts file where it for example export class MyType { ... }. Then the package also has a namespace.d.ts looking like this:

import _MyNamespace from './types';

export as namespace MyNamespace;
export = _MyNamespace;

declare global {
    const MyNamespace: typeof _MyNamespace;
}

In our code, we then have a MyNamespace.d.ts that references the namespace file:

/// <reference path="../node_modules/@user/package/namespace.d.ts" />

Any idea how to make it work in v5.5 without wrapping everything in InstanceType<typeof ...>?