Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSDoc equivalent of import * #41825

Closed
5 tasks done
Gozala opened this issue Dec 5, 2020 · 16 comments · Fixed by #57207
Closed
5 tasks done

JSDoc equivalent of import * #41825

Gozala opened this issue Dec 5, 2020 · 16 comments · Fixed by #57207
Labels
Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@Gozala
Copy link

Gozala commented Dec 5, 2020

Search Terms

  • jsdoc import
  • import *
  • import namespace
  • jsdoc namespace

Suggestion

Provide a JSDoc equivalent of import * as Sub from "./sub" so that types & interfaces from the ./sub module could be referenced as Sub.Type, Sub.Interface.

I have no preference in terms of actual syntax, but in my experience general expectation had been that following syntax should do just that (but it is not and I can't seem to figure out what does Sub results in here:

@typedef {import('./sub')} Sub

If above is not a viable options maybe using same * character could be an options:

@typedef {import('./sub').*} Sub

Or maybe whole new @import jsdoc tag.

Use Cases

js-ipfs (fairly large code base) has adopted TS via JSDoc syntax but dealing with large number of imported types & interfaces from other modules had been a major source of pain for following reasons:

  1. Every single type requires @typedef {import('...').Name} Alias.
  2. Importing generic types / interfaces requires repeating type parameters via @template tags
  • Which is error prone because when omitted it turns into any
  • Ends up require lot of typing
  1. Changes to library layout requires updating all those typdefs all over the code base (and vscode can't help there sadly)
    • Alternative is to consolidating them in one place, but then vscode (and alike) takes multiple hops / clicks to get you to actual type definition.

All of the above make otherwise mostly good experience to be painful.

Current approach also has side effect of turning imported interfaces into types (see #41258) which than can't be used in implements syntax as they are no longer interfaces.

Given that it is possible to do import * as Sub from "./sub" to get a namespace of exports in TS syntax it seems like equivalent in jsdoc syntax would:

  1. Allow reduced number of @typedef {import(...).Name} Name declarations.
  2. Allow importing generic types without having to retype type parameters via @temaplate tags.
  3. Avoid turning interfaces.

Examples

Consider following TS code:

import { BlockEncoder, BlockDecoder } from "@multiformats/codecs"
import { DagNode } form "./node"

class DagPB implements BlockEncoder<0x70, DagNode>, BlockDecoder<0x70, DagNode> {
  async encode(node:DagNode):Promise<Uint8Array> {
     // ...
  }
  async decode(bytes:Uint8Array):Promise<DagNode> {
    // ...
  }
}

Same code with JSDoc syntax turns into following:

/**
 * @template {number} Code
 * @template T
 * @typedef {import('@multiformats/codecs').BlockEncoder<Code, T>} BlockEncoder
 */
/**
 * @template {number} Code
 * @template T
 * @typedef {import('@multiformats/codecs').BlockDecoder<Code, T>} BlockDecoder
 */
/**
  * @typedef {import("./node").DagNode} DagNode
  */

/**
 * @implements {BlockEncoder<0x70, DagNode>}
 * @implements {BlockDecoder<0x70, DagNode>}
 */
class DagPB {
  /**
   * @param {DagNode} node
   * @returns {Promise<Uint8Array>}
   */
  async encode(node) {
     // ...
  }
  /**
   * @param {Uint8Array} bytes
   * @returns {Promise<DagNode>}
   */
  async decode(bytes) {
    // ...
  }
}

Note: Above code won't even produce desired typedefs due to #41258

And now if we had what this issue proposes it would be:

/**
 * @typedef {import('@multiformats/codecs')} Codec
 * @typedef {import('./node').DagNode} DagNode
 */

/**
 * @implements {Codec.BlockEncoder<0x70, DagNode>}
 * @implements {Codec.BlockDecoder<0x70, DagNode>}
 */
class DagPB {
  /**
   * @param {DagNode} node
   * @returns {Promise<Uint8Array>}
   */
  async encode(node) {
     // ...
  }
  /**
   * @param {Uint8Array} bytes
   * @returns {Promise<DagNode>}
   */
  async decode(bytes) {
    // ...
  }
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Dec 7, 2020
@RyanCavanaugh
Copy link
Member

cc @sandersn for visibility

@Raynos
Copy link

Raynos commented Jan 8, 2021

Ran into a similar related issue today

I attempted to do the following

/** @typedef {import('../../types.js')} T */

/**
 * @return {Promise<T.Result<any>>}
 */
module.exports = async (args) => { ... }

Where types.js is

/**
 * @template T
 * @typedef {{
 *    err: Error,
 *    data?: undefined
 * } | {
 *    err?: undefined,
 *    data: T
 * }} Result<T>
 */

module.exports = {}

And then I got

'T' only refers to a type, but is being used as a namespace here.

I wanted to treat the entire package as a namespace so that I can import it and reference types inside of it using the import() function syntax, but it seems like that only returns types and not namespaces.

@Raynos
Copy link

Raynos commented Jan 8, 2021

@Gozala

Due to random dumb luck and just trying stuff I discovered that you can declare ... in a project ( https://github.com/voxpelli/types-in-js/blob/main/FAQ.md#i-want-to-share-some-helper-interfaces-or-aliases-globally-in-my-jsdoc-project ).

You may be able to declare that various types exists in @multiformats/codecs/declare.d.ts and then just include that in jsconfig / tsconfig so that those tokens are available in a global scope.

You can also use namespaces to namespace codecs { interface BlockEncoder }

This does not solve any of the importing use cases, however it turns out that the author whom writes the exports can export into a global namespace which can be used anywhere without import syntax.

@Gozala
Copy link
Author

Gozala commented Jun 15, 2021

Poor mans solution I've settled on is along these lines:

lib.js (implementation lives here)

import * as API from "./result.js"
/**
 * @template T
 * @param {T} value
 * @returns {API.Result<never, T>}
 */
export ok = (value) => ({ ok: true, value })

result.js (just a facade for lib.js)

export * from "./lib.js"

result.ts (shadow .js to add types)

export * from "./lib.js"
export type Result<X, T> =
  | { ok: true, value: T }
  | { ok: false, error: X }

Now any other module could do following:

import * as Result form "../path/to/result.js"

/**
 * @template X, T
 * @param {Result.Result<X, T>} result
 * @param {(t:T) => U} f
 * @returns {Result.Result<X, U>}
 */
export const map = (result, f) =>
  result.ok ? Result.ok(f(result.value)) : result

@Jamesernator
Copy link

Jamesernator commented Sep 23, 2021

Just to clarify what the issue actually is, essentially this:

/**
  * @typedef {import("./someModule.js")} SomeModule
  */

is actually equivalent to this:

/**
  * @typedef {typeof import("./someModule.js")} SomeModule
  */

In TypeScript however the following are distinct in general:

import type * as SomeModule from "./someModule.js";
type SomeModule = typeof import("./someModule.js");

@Gozala
Copy link
Author

Gozala commented Jan 19, 2022

Hey Team,

Any updates on this I keep introducing facade files just to workaround retyping all the generics at every import, but it really feels like there needs to be a proper solution. I also came across #22160 which seems to be asking fro the same feature with different syntax.

@Thaina
Copy link

Thaina commented Feb 24, 2022

Not sure is it related but it seem the @typedef was not really registered a symbol as a type

This affect constructor that I can't see the signature and autocomplete on the constructor

@Gozala
Copy link
Author

Gozala commented Feb 25, 2022

@RyanCavanaugh @sandersn any updates on this ? This is probably the most annoying limitation when using TS with JSDoc.

If someone is willing to mentor me on this, I’d be up to put my time into this.

@Thaina
Copy link

Thaina commented Mar 3, 2022

edit: It seem not the case I talk about in previous comment

My problem is actually @typedef is not working as expected

Using @type is what I expected

image

But @typedef didn't

image

Using @var also not the solution

image

@toFrankie

This comment was marked as off-topic.

@lacherogwu

This comment was marked as off-topic.

@sandersn
Copy link
Member

We decided to accept this issue. Here's an outline of my thoughts for anybody that wants to try implementing this:

  1. Syntax:
    • @importType followed by whatever import syntax can follow import type in Typescript. For example, @importType { Component } from '@angular/core' or @importType * as Types from './types'
    • Like @typedef/@callback, this is a top-level, standalone tag.
    • Re-use the existing import parsing code as much as possible, because this will make the rest of the implementation smaller.
  2. Semantics: same as import type as much as possible. The checker already falls back to JSDoc for things like @type, @overload, @private, etc. The code for @importType will be similar, but there will be lots of places that need to change; imports are used all over.
  3. Specifically, the services layer will need lots of tests to make sure that find-all-refs, goto-def and everything else work with @importType the same as they do for import type. Auto-imports will be especially tricky. I have no idea how auto-imports work in JS right now, but this tag would theoretically provide feature parity; generate import for values and @importType for types.

That's very high level. I likely skimmed over lots of work in saying that the compiler needs to provide the same semantics as import type, so feel free to talk through followup issues here.

@sandersn sandersn added Experience Enhancement Noncontroversial enhancements and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Mar 20, 2023
@sandersn sandersn added this to the Backlog milestone Mar 20, 2023
@boneskull
Copy link
Contributor

@sandersn I'll buy you a sandwich when this lands

@what1s1ove
Copy link

what1s1ove commented Dec 14, 2023

Hope it will handle such cases with generics. Or maybe you know another way to handle it without importing type right in the label type key.

// values-of.type.js

/**
 * @template T
 * @typedef { T[keyof T] } ValuesOf
 */
// settings-button-payload.type.js

// error ValuesOf is a generic 
/** @typedef {import('../types/types.js').ValuesOf} ValuesOf */
/** @typedef {typeof import('../enums/enums.js').SettingButtonLabel} SettingButtonLabel */

/**
 * @typedef {{
 * 	isDefaultChecked: boolean
 * 	label: ValuesOf<SettingButtonLabel>
 * }} SettingsButtonPayload
 */

@what1s1ove
Copy link

what1s1ove commented Dec 25, 2023

Hope it will handle such cases with generics. Or maybe you know another way to handle it without importing type right in the label type key.

// values-of.type.js

/**
 * @template T
 * @typedef { T[keyof T] } ValuesOf
 */
// settings-button-payload.type.js

// error ValuesOf is a generic 
/** @typedef {import('../types/types.js').ValuesOf} ValuesOf */
/** @typedef {typeof import('../enums/enums.js').SettingButtonLabel} SettingButtonLabel */

/**
 * @typedef {{
 * 	isDefaultChecked: boolean
 * 	label: ValuesOf<SettingButtonLabel>
 * }} SettingsButtonPayload
 */

I just found a way to fix it by reading the 'The Saga of the Closure Compiler, and Why TypeScript Won' article. The method is quite specific. 🤯

// values-of.type.js

/**
 * @template T
 * @typedef {T[keyof T]}
 */
let ValuesOf

export { ValuesOf }
// settings-button-payload.type.js

import { ValuesOf } from '~/libs/types/types.js'

/** @typedef {typeof import('../enums/enums.js').SettingButtonLabel} SettingButtonLabel */

/**
 * @typedef {{
 * 	isDefaultChecked: boolean
 * 	label: ValuesOf<SettingButtonLabel>
 * }} SettingsButtonPayload
 */

@remcohaszing
Copy link

I addition to @importType, I think an analogous @exportType would be nice too.

Within unifiedjs we use a lot of re-exports to expose the public interface including types. An example is https://github.com/mdx-js/mdx/blob/3.0.1/packages/mdx/index.js.

This code is equivalent to:

export type Fragment = import('./lib/util/resolve-evaluate-options.js').Fragment
// etc

In TypeScript you would write this as:

export { Fragment } from './lib/util/resolve-evaluate-options.js'
// etc

This results in a loss of descriptions of those types for the users. Plus generics need to be redefined.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.