In JS, jsdoc should be able to declare functions as overloaded #25590
Comments
How would one provide comments and parameter descriptions ( |
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. |
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. |
Here's a type that is too permissive, but it's pretty close: In order to express dependencies between types without overloads, you need conditional types:
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. |
@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 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. |
Another suggestion from the VS user voice: https://developercommunity.visualstudio.com/content/idea/425961/support-3.html |
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. |
#30940 points out that Jsdoc has an established format for representing overloads. |
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? |
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 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. |
So the key piece is basically the typedef with an intersection type
|
You can also do: /** @typedef {{
(event: 'error', listener: (error: Error) => void): this;
(event: 'response_required', listener: (token: string) => void): this;
}} DiamondEvent */ |
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 Is this issue likely to see any progress in the near future? It would really help with adding type definitions to JavaScript projects. |
It sounds like #25590 (comment) suggests that Closure supports intersection types ( I tried the run-together block comment syntax and it works. Per englercj/tsd-jsdoc#30 you must tag each block with a different |
Every info I find in this topic suggests using the /**
* 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. |
Unfortunately none of the solutions above work with js ES6 class functions. ESlint and VSCode can't seem to process a no beuno:
if you use a traditional 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. |
@chriseaton Also, your code has a bug when The implementation should instead be using the nullish‑coalescing ( multSq(x, y) {
return x * (y ?? x);
} |
@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 |
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. |
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. interface ReadableStreamGetReader<R> {
getReader(options: { mode: "byob" }): ReadableStreamBYOBReader;
getReader(): ReadableStreamDefaultReader<R>;
} I have not found any way to implement it or subclass it |
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 |
Not exactly, because following signature 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. |
@Gozala |
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 |
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:
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 |
The subject of this issue is so obvious, isn't it? 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! |
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; |
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
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.The text was updated successfully, but these errors were encountered: