The Wayback Machine - https://web.archive.org/web/20210611124812/https://github.com/microsoft/TypeScript/issues/25590
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

In JS, jsdoc should be able to declare functions as overloaded #25590

Open
sandersn opened this issue Jul 11, 2018 · 31 comments
Open

In JS, jsdoc should be able to declare functions as overloaded #25590

sandersn opened this issue Jul 11, 2018 · 31 comments

Comments

@sandersn
Copy link
Member

@sandersn sandersn commented Jul 11, 2018

Search Terms

jsdoc overloads

Suggestion

You can specify overloads in Typescript by providing multiple signatures. You should be able to do the same thing in Javascript. The simplest way I can think of is to allow a type annotation on a function declaration.

Examples

/** @typedef {{(s: string): 0 | 1; (b: boolean): 2 | 3 }} Gioconda */

/** @type {Gioconda} */
function monaLisa(sb) {
    // neither overloads' return types are assignable to 1 | 2, so two errors should be logged
    return typeof sb === 'string' ? 1 : 2;
}

/** @type {2 | 3} - call should resolve to (b: boolean) => 2 | 3, and not error */
var twothree = monaLisa(false);

// js special property assignment should still be allowed
monaLisa.overdrive = true;

Note that checking parameters against the implementation parameters will be trivial since there's no obvious type but any for the implementation parameters. Checking of return types against the implementation return type will have to use the return type of the function body as the implementation return type. That check will also likely be trivial since function-body return types usually depend on parameters.

@AlCalzone
Copy link

@AlCalzone AlCalzone commented Aug 17, 2018

How would one provide comments and parameter descriptions (/** @param */) when doing this?
Sometimes it is necessary to highlight the differences between overloads. In .d.ts files this works by annotating each single overload with its own comment. See also #407

@AlCalzone
Copy link

@AlCalzone AlCalzone commented Aug 17, 2018

One way I could think of is

/**
 * Does something
 * @callback foo_overload1
 * @param {number} arg1 Something
 */

 /**
 * Does something else
 * @callback foo_overload2
 * @param {string} arg1 Something else
 * @param {number} arg2 Something
 */

/** @type {foo_overload1 | foo_overload2} */
function foo(arg1, arg2) {

}

but this way, we get an error on the type annotation that the signatures don't match.

@AlCalzone
Copy link

@AlCalzone AlCalzone commented Nov 23, 2018

Too bad this seems pretty low on your priorities :( I just ran into an issue where it is impossible to document a method signature in JavaScript due to this. I have a function where all arguments except the last one are optional:

// valid overloads:
declare function test(arg1: string, arg2: number, arg3: () => void): void;
declare function test(arg2: number, arg3: () => void): void;
declare function test(arg1: string, arg3: () => void): void;
declare function test(arg3: () => void): void;

I'd be really glad if documenting this via JSDoc would be possible in the near future.

@sandersn
Copy link
Member Author

@sandersn sandersn commented Nov 26, 2018

Here's a type that is too permissive, but it's pretty close:
declare function test(arg123: string | number | () => void, arg23?: number | () => void, arg3?: () => void): void

In order to express dependencies between types without overloads, you need conditional types:

declare function test<T extends string | number | () => void>(
    arg123: T, 
    arg23?: T extends string ? number : 
            T extends number ? () => void : 
            never, 
    arg3?: T extends string ? () => void : never): void

But conditional type syntax doesn't work in JS so you'd have to write a series of type aliases in a .d.ts and reference them.

@AlCalzone
Copy link

@AlCalzone AlCalzone commented Nov 26, 2018

@sandersn Thanks for the explanation, but it does not quite achieve what I'm trying to do.

I have a JS CommonJS module which contains said function with a couple of overloads similar to the ones I mentioned above. Inside the module, this function is heavily used so I want to give it proper type checking and document the parameters. The function is not exported.

The project maintainer does not use TypeScript, but lets me work with JSDoc comments to get type checking support. Asking him to move every overloaded function into separate modules and add .d.ts files for them is too much to ask.

When annotating the function with @type {(arg1: union | of | types, ...) => void}, the type checker fails to understand the type of variables inside the function and unnecessarily complains about seemingly disallowed assignments. Also, I cannot provide additional explanation to the single parameters like I would do in an overloaded declaration + JSDoc @param comments.

The external .d.ts file does not work, even if I hack something like this:

// file1.d.ts
export declare function test(arg1: string, arg2: number, arg3: () => void): void;
export declare function test(arg2: number, arg3: () => void): void;
export declare function test(arg1: string, arg3: () => void): void;
export declare function test(arg3: () => void): void;
// ^ the function is NOT exported

// file1.js
/** @type {typeof import("./file1").test} */
// ^ ERROR: The type of a function declaration must match the function's signature. [8030]
function test(arg1, arg2, arg3) {
    return;
}

I could live with the signature being to permissive, but in this situation, I'm unable to document the parameter types without "breaking" the implementation.

@sandersn
Copy link
Member Author

@sandersn sandersn commented Jan 16, 2019

@AlCalzone
Copy link

@AlCalzone AlCalzone commented Feb 16, 2019

I can't believe this does not get more feedback. This is a pain in the a** to work with in legacy codebases with overloaded callback-style APIs.

@sandersn
Copy link
Member Author

@sandersn sandersn commented Jul 25, 2019

#30940 points out that Jsdoc has an established format for representing overloads.

@jdatskuid
Copy link

@jdatskuid jdatskuid commented Aug 12, 2019

What about situations where I have a different number of arguments. For example:

/**
 * @param {Object} row - The row
 * @param {String} field - Field name
 * @param {*} value - Field value
 */
foo( row, field, value  )

and

/**
 * @param {Object} row - The row
 * @param {Object} fieldValues - Key value pair of fields to values
 */
foo( row, fieldValues  )

The recommended approach appears to be:

/** @type {((row: Object, field: String, value: any) => Object) & ((row: Object, fieldValues: Object) => Object)} */

But where are all my lovely argument descriptions going to reside? How do I communicate that the third parameter is only valid if the second parameter is a string?

@systemmonkey42
Copy link

@systemmonkey42 systemmonkey42 commented Dec 4, 2019

I have been using the following syntax(es) for overloading the event handling for a class which inherits "EventEmitter"

class Diamond extends EventEmitter {
    constructor() {
         / TYPE DEFINITION OF DiamondEvent HERE
        /** @type {DiamondEvent} */
        this.on;
    }
};

The following all appear to work equally well. Its up to you as to which is more readable.

Closure style, multiple types merged

/**
 * @typedef {(event: 'error', listener: (error: Error) => void) => this} EventError
 * @typedef {(event: 'response_required', listener: (token: string) => void) => this} EventResponseRequired
 * @typedef {EventError & EventResponseRequired} DiamondEvent
 */

Callback style

/**
 * @callback EventError
 * @param {'error'} event
 * @param {(error: Error)=>void} listener
 * @return this
 * @callback EventResponseRequired
 * @param {'response_required'} event
 * @param {(token: string)=>void} listener
 * @return this
 * @typedef {EventError & EventResponseRequired} DiamondEvent
 */

Javascript function style, multiple types merged

/**
 * @typedef {function('error',function(Error):void):this} EventError
 * @typedef {function('response_required',function(string):void):this} EventResponseRequired
 * @typedef {EventError & EventResponseRequired} DiamondEvent
 */

Closure style, combined types

/**
 * @typedef {((event: 'error', listener: (err: Error) => void) => this) & ((event: 'response_required', listener: (token: string) => void) => this)} DiamondEvent
 */

Javascript style, combined types

/**
 * @typedef {(function('error',function(Error):void):this) & (function('response_required',function(string):void):this)} DiamondEvent
 */

When I press the '(' after typing on, the autocomplete popup looks like this

let dm = new Diamond()

           on(event: "error", listener: (error: Error) => void): Diamond
           on(event: "response_required", listener: (token: string) => void): Diamond
dm.on(|

and my editor correctly identifies the parameter types based on the the event name in the first parameter.

I hope this helps anyone trying to clean deal with functions which have multiple prototypes. Until I figured this out, I needed to write a '.d.ts' file to describe the functions. This is so much cleaner.

@AlCalzone
Copy link

@AlCalzone AlCalzone commented Dec 4, 2019

So the key piece is basically the typedef with an intersection type

@typedef {EventError & EventResponseRequired} DiamondEvent
@ExE-Boss
Copy link
Contributor

@ExE-Boss ExE-Boss commented Dec 4, 2019

You can also do:

/** @typedef {{
	(event: 'error', listener: (error: Error) => void): this;
	(event: 'response_required', listener: (token: string) => void): this;
}} DiamondEvent */
@glen-84
Copy link

@glen-84 glen-84 commented May 28, 2020

#30940 points out that Jsdoc has an established format for representing overloads.

@sandersn,

This format is used by the xlsx-populate project (example). I was looking at adding type definitions to that project, but this issue prevents that from being possible (there are 77 instances of *//**, and I wanted to avoid making too many changes to the JSDoc comments).

Is this issue likely to see any progress in the near future? It would really help with adding type definitions to JavaScript projects.

@thw0rted
Copy link

@thw0rted thw0rted commented May 29, 2020

It sounds like #25590 (comment) suggests that Closure supports intersection types (A & B) but it's a syntax error when I use tsd-jsdoc.

I tried the run-together block comment syntax and it works. Per englercj/tsd-jsdoc#30 you must tag each block with a different @variation but once you do that, the generated typings are correct. This is a strong argument in favor of Typescript natively supporting the same construct, IMHO.

@mxrsoon
Copy link

@mxrsoon mxrsoon commented Aug 21, 2020

#30940 points out that Jsdoc has an established format for representing overloads.

Every info I find in this topic suggests using the *//** syntax to document function overloads. Something like that:

/**
 * Do something with numbers.
 * @param {number} a - First number parameter.
 * @param {number} b - Second number parameter.
 * @returns {number} Result as number.
 *//**
 * Do something with strings.
 * @param {string} a - First string parameter.
 * @param {string} b - Second string parameter.
 * @param {string} c - Third string parameter.
 * @returns {string} Result as string.
 */
function something(a, b, c) {
    ...
}

Hope this gets implemented soon, as overloading is a basic and widely used technique.

@chriseaton
Copy link

@chriseaton chriseaton commented Oct 28, 2020

Unfortunately none of the solutions above work with js ES6 class functions. ESlint and VSCode can't seem to process a @type on an es6 function.

no beuno:

class Person {
    /**
     * @callback MultSq1
     * @param {Number} x
     * @returns {Number}
     */
    /**
     * @callback MultSq2
     * @param {Number} x
     * @param {Number} y
     * @returns {Number}
     */

    /**
     * @type {MultSq1 & MultSq2}      <--ESLint error, VSCode shows the attribute but doesn't show overloads.
     */
    multSq(x, y) {
        return x * (y || x);
    }
}

if you use a traditional function or arrow function with a variable - it will show an overload.

The only trick I found around this is to set the type from a const function outside the class then set a property in the constructor to that const function... unfortunately it shows a blue property icon in intellisense instead of the pink function icon. Not sure what to do about that yet.

@ExE-Boss
Copy link
Contributor

@ExE-Boss ExE-Boss commented Oct 28, 2020

@chriseaton
Well, ESLint’s built‑in JSDoc validation is broken, and eslint‑plugin‑jsdoc has incomplete support for TypeScript types: jsdoctypeparser/jsdoctypeparser#50.


Also, your code has a bug when y === 0.

The implementation should instead be using the nullish‑coalescing (??) operator:

multSq(x, y) {
	return x * (y ?? x);
}
@chriseaton
Copy link

@chriseaton chriseaton commented Oct 28, 2020

@ExE-Boss I was pointing out that while the above solutions have workarounds for function vars, they don't work for ES6 class methods. The title of this issue again: "In JS, jsdoc..." as far as I know ES6 classes and methods are finished proposals and officially part of JS.


Also, it's not a bug if you expect multSq to square when y is 0 - it's all about use-case if we wanna dive into the logic of my quick and dirty sample 😑 which... why bother?

@JoakimCh
Copy link

@JoakimCh JoakimCh commented Dec 5, 2020

#30940 points out that Jsdoc has an established format for representing overloads.

Every info I find in this topic suggests using the *//** syntax to document function overloads. Something like that:

/**
 * Do something with numbers.
 * @param {number} an - First number parameter.
 * @param {number} bn - Second number parameter.
 * @returns {number} Result as number.
 *//**
 * Do something with strings.
 * @param {string} as - First string parameter.
 * @param {string} bs - Second string parameter.
 * @param {string} cs - Third string parameter.
 * @returns {string} Result as string.
 */
function something(a, b, c) {
    ...
}

Hope this gets implemented soon, as overloading is a basic and widely used technique.

This would be the most important syntax to support. It's a clean and readable way to define overloads and it is the correct Jsdoc syntax for it.

@Gozala
Copy link

@Gozala Gozala commented Jan 13, 2021

Lack of overload support makes it virtually impossible to implement certain interfaces that use overloads or subclass classes that are defined with overloads e.g. ReadableStream<T>. Please consider following subset of the interface

interface ReadableStreamGetReader<R> {
    getReader(options: { mode: "byob" }): ReadableStreamBYOBReader;
    getReader(): ReadableStreamDefaultReader<R>;
} 

I have not found any way to implement it or subclass it

@thw0rted
Copy link

@thw0rted thw0rted commented Jan 13, 2021

Isn't that really more of a problem with the assignability of overloads in the first place, rather than the fact that JSDoc doesn't let you specify them? Overloads are supposed to be purely syntactic sugar, right? It seems crazy to me that

class Readable<T> extends ReadableStream<T> {
  getReader(options: {mode: "byob"}): ReadableStreamBYOBReader
  getReader(): ReadableStreamDefaultReader<T>
  getReader(options?: {mode: "byob"}): ReadableStreamDefaultReader<T> | ReadableStreamBYOBReader {
    if (options?.mode === "byob") {
      return new ReadableStreamBYOBReader()
    } else {
      return new ReadableStreamDefaultReader()
    }
  }
}

works, but simply removing the two overload signatures and leaving the merged signature causes it to fail. At first thought this might be a problem with strictFunctionTypes, but disabling that check doesn't get rid of the error.

@Gozala
Copy link

@Gozala Gozala commented Jan 15, 2021

Isn't that really more of a problem with the assignability of overloads in the first place, rather than the fact that JSDoc doesn't let you specify them? Overloads are supposed to be purely syntactic sugar, right? It seems crazy to me that

Not exactly, because following signature getReader(options?: {mode: "byob"}): ReadableStreamDefaultReader<T> | ReadableStreamBYOBReader is not equivalent of:

getReader(options: {mode: "byob"}): ReadableStreamBYOBReader
getReader(): ReadableStreamDefaultReader<T>

Since former signature says that return value is a union regardless of the argument passed, while later one allows is more specific and return type is infer based on the input. More accurate signature expressing the same would be:

getReader <Args extends []|[{mode:"byob"}>
  (...args:Args): Args extend [{mode:"byob"] ? ReadableStreamBYOBReader : ReadabelStreamDefaultReader<T>

However it is far more complicated and less intuitive way to define what the interface for the function is than override syntax.

@ExE-Boss
Copy link
Contributor

@ExE-Boss ExE-Boss commented Jan 17, 2021

@Gozala
Your code is broken in the case when options is passed without a mode property.

@thw0rted
Copy link

@thw0rted thw0rted commented Jan 18, 2021

What I meant was, when you define a TS overload, you have to include all the signatures you actually intend to call, then one "hidden" signature -- the last one -- to which all the other signatures can be assigned. (I think there's a special name for the last signature?) In my experience, rather than trying to get clever with conditional generics, typically this is declared as a simple superset that combines all the inputs and outputs, and runtime checks are used to determine what's what.

Look at the handbook example for overloading -- they can't even be bothered with a union type, they just declare the argument as any. This more-detailed example still only uses unions, not generics. I can't find anybody who uses generics to enforce the same behavior as the overload signature the way that your example does.

@Gozala
Copy link

@Gozala Gozala commented Jan 18, 2021

I am not sure what are we debating here. All I am saying is that without some support for overloaded signatures in JSDoc it is:

  1. Impossible to implement interface, without loosening type signatures (as in if I loosen type signatures to any or some other loose type).
  2. Impossible to subclass various built-ins that are defined with overloads, without loosening type signatures.

Which is why I am advocating for adding support. Whether generics used in examples are common is irrelevant to that point. So is bug in the that @ExE-Boss pointing out, if anything it proves my point that overloads are a lot more intuitive way to express input - output type relationships as opposed to fancy utility types

@stanf0rd
Copy link

@stanf0rd stanf0rd commented Feb 21, 2021

The subject of this issue is so obvious, isn't it?
Intellisense engine just chooses first JSDoc comment in list of overloads and ignores any other.
It should at least choose last JSDoc (above implementation) as main one.
It looks more like a bug, not a suggestion.

Disputes about best ways to implement comments overloads are absolutely reasonable, unlike of disputes which way of using overloads is better 🤷‍♂️

So strange that this issue is already 2.5 years old!

@andiby
Copy link

@andiby andiby commented Mar 4, 2021

As a workaround these declarations for overloads already work for a long time:

/**
@typedef {{
    (s: string): 0 | 1
    (b: boolean): 2 | 3
    [k:string]: any
}} Gioconda
*/

/** @type {Gioconda} */
const monaLisa = (
/** @param {string|boolean} sb */
(sb) => {
    return typeof sb === 'string' ? 1 : 2;
});

const obj = {
    /** @type {Gioconda} */
    monaLisa: (
    /** @param {string|boolean} sb */
    (sb) => {
        return typeof sb === 'string' ? 1 : 2;
    })
};

class MonaLisa {
    /** @type {Gioconda} */
    monaLisa = (
    /** @param {string|boolean} sb */
    (sb) => {
        return typeof sb === 'string' ? 1 : 2;
    })
}

/** @type {2 | 3} - call resolve to (b: boolean) => 2 | 3 */
var twothree = monaLisa(false);
/** @type {2 | 3} - call resolve to (b: boolean) => 2 | 3 */
var twothreeObj = obj.monaLisa(false);
/** @type {2 | 3} - call resolve to (b: boolean) => 2 | 3 */
var twothreeClass = new MonaLisa().monaLisa(false);

// js special property assignment should still be allowed
monaLisa.overdrive = true;
obj.monaLisa.overdrive = true;
MonaLisa.prototype.monaLisa.overdrive = true;

Playground Link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet