Skip to content

JSDoc Typedef Syntax for Overloadable ConstructorsΒ #55916

Closed
@ITenthusiasm

Description

@ITenthusiasm

πŸ” Search Terms

constructor jsdoc overload

βœ… Viability Checklist

  • 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, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

⭐ Suggestion

Currently, with pure TypeScript, it's possible to create an overloaded, generic constructor type. (The benefits of overloaded generic constructors are better detailed in #40451.) However, there is no similar concept available in JSDocs. After migrating a TS project to JSDocs, I found this to be a hindrance. (There are some workarounds, but they still negatively impact user experience surrounding imports.)

It would be great if this use case was supported in JSDocs land. Alternatively, if #40451 was resolved, I'm assuming that this issue would be inherently be resolved as well. Rich Harris made a compelling case for using JSDocs in his interview with the Primeagen and his interview with Kev from Svelte Society. It seems there are more library authors interested in reaching for JSDocs (through TS) for simplicity (myself included); so greater support in situations like these would be fantastic.

πŸ“ƒ Motivating Example

I'm appealing to #40451 and the example in #52585 here. If I want to create a DOM-watcher class in TypeScript that will have a consistent, unchanging interface and that can be constructed in different ways, I need a generic constructor:

type EventType = keyof DocumentEventMap;
type ObserverEvent<T extends EventType> = DocumentEventMap[T];
type ObserverEventListener<T extends EventType> = (event: ObserverEvent<T>) => unknown;

/* -------------------- Base Observer -------------------- */
interface BaseObserverConstructor {
  new <T extends EventType>(type: T, listener: ObserverEventListener<T>): BaseObserver;
  new <T extends ReadonlyArray<EventType>>(types: T, listener: ObserverEventListener<T[number]>): BaseObserver;
}

interface BaseObserver {
  observe(element: HTMLElement): void;
  unobserve(element: HTMLElement): void;
  disconnect(): void;
}

export const BaseObserver: BaseObserverConstructor = class<T extends EventType | ReadonlyArray<EventType>> {
  constructor(typeOrTypes: T, listener: ObserverEventListener<EventType>) {
    // Do something with arguments
  }

  observe(element: HTMLElement): void {
    // Observe the element
  }

  unobserve(element: HTMLElement): void {
    // Undo the observation
  }

  disconnect(): void {
    // `unobserve` everything
  }
};

This works fine in TS, but it's incompatible with JSDocs -- causing friction during migration. If JSDoc support was added, perhaps we'd probably have something like the following:

/** @typedef {keyof DocumentEventMap} EventType */
/** @template {EventType} T @typedef {DocumentEventMap[T]} ObserverEvent */
/** @template {EventType} T @typedef {(event: ObserverEvent<T>) => unknown} ObserverEventListener */

/* -------------------- Base Observer -------------------- */
/**
 * @constructor BaseObserverConstructor
 * // Define overloads that create a `BaseObserver` instance
 */

/**
 * @typedef {Object} BaseObserver
 * // Define types
 */

/** 
 * @template {EventType | ReadonlyArray<EventType>} T
 * @type {BaseObserverConstructor}
 */
export const BaseObserver = class {
  /**
   * @param {T} typeOrTypes
   * @param {ObserverEventListener<EventType>} listener
   */
  constructor(typeOrTypes, listener) {
    // Do something with arguments
  }

  /** @param {HTMLElement} element @returns {void} */
  observe(element) {
    // Observe the element
  }

  /** @param {HTMLElement} element @returns {void} */
  unobserve(element) {
    // Undo the observation
  }

  /** @returns {void} */
  disconnect() {
    // `unobserve` everything
  }
};

That said, this can also be solved by resolving #40451. Resolving said issue would remove the need for any additional TS-specific JSDoc syntax. However, if that is not up for consideration, then a JSDoc syntax that mimics TypeScript's behavior would be very helpful.

πŸ’» Use Cases

1) What do you want to use this for?

The example above is probably the clearest example of what I need this for. This is for a project that I'm actively working on.

2) What shortcomings exist with current approaches?

The current shortcoming is that this isn't possible for those only wanting to use JSDocs. This creates some minor friction in how a project structures its exports (in package.json) and how a project structures its filesystem. (I didn't require extra .d.ts files before migrating to JSDocs.) It also separates a class method's documentation (defined in an interface in a .d.ts file) from its implementation (defined in a js file) -- resulting in lesser maintainability.

3) What workarounds are you using in the meantime?

The current workaround is to create a separate <FILE_NAME>Types.d.ts file that has the TS Generic Constructor defined. The constructor can be imported from the types file by the JS file. This would need to be done for every JavaScript file. Alternatively, a global types file for the application/library being built can be created, and the types would be imported collectively from there instead.

This does add some complexity for library maintainers using package.json's exports property, however.

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions