microsoft/TypeScript

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

sandersn opened this issue · 43 comments

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.

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

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:
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.

@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.

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.

Function overloading can be done using:

/** @type {((name: string) => Buffer) & ((name: string, encoding: string) => string))} */
const readFile = (name, encoding = null) => {  }

I discovered this purely by accident.

@ExE-Boss, your solution doesn't work for me. It says Parameter 'name' implicitly has an 'any' type.ts(7006) and Parameter 'encoding' implicitly has an 'any' type.ts(7006)

That’s how overloads work, the implementation takes any for each parameter.

You need to do:

/** @type {((name: string) => Buffer) & ((name: string, encoding: string) => string))} */
const readFile = (/** @type {string} */ name, /** @type {string | null} */ encoding = null) => {  };

To set the types for the actual implementation.

It’d be nice if TypeScript was able to infer the argument types from the function intersection type as unions.

This however becomes a lot harder with overloads that take much more different sets of arguments:

/**
 * @callback TestCallback
 * @return {void | PromiseLike<void>}
 *
 * @typedef {object} TestOptions
 * @property {number} [timeout]
 */

/**
 * @type {((cb: TestCallback) => void) & ((description: string, cb: TestCallback) => void) & ((options: TestOptions, cb: TestCallback) => void) & ((description: string, options: TestOptions, cb: TestCallback) => void)}
 */
const test = (arg0, arg1, arg2) => {  };

or

/** @type {((num: number, obj: {stuff?: unknown}) => bigint) & ((str1: string, str2: string) => object)} */
const doStuff = (arg0, arg1) => {  };

Just as an alternative for syntax, what about accepting multiple @type? This would be fairly similar to how it looks in TypeScript.

/**
  * @type {(x: string) => number}
  * @type {(x: number) => string}
  * @param {number | string} x
  */
function flipType(x) {
  if (typeof x === 'string') {
    return Number(x);
  } else {
    return String(x);
  }
}

#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 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.

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

@typedef {EventError & EventResponseRequired} DiamondEvent

You can also do:

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

#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.

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.

#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.

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.

@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);
}

@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?

#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.

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

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.

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.

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

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.

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

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!

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

Unfortunately, the workarounds described here no longer work due to #44693.

You can try this:

/**
 * @type {{
 * (s:string) => 1;
 * (b:boolean) => 2;
 * }}
 */
const monaLisa = (sb) => {
    return typeof sb === 'string' ? 1 : 2;
}
let two = monaLisa(false);   // vscode can recognize that this is 2
let one = monaLisa("m");     // this is 1

You can not use function monaLisa() to declare it. I think this is a defect.
In class, you can:

class A {
    /**
     * @type {{
     * (s:string) => string;
     * (n:number) => number;
     * }}
     */
    func = (sn) => {}
}
let n = (new A()).func(0);
let s = (new A()).func("s");

But, A.func is a property, not a method in vscode.

I'm doing (string, callback) => {} type overloading like this:

/**
 * @typedef {Object} CallbacksMap
 * @property {MouseEvent} click
 * @property {DragEvent} drag
 */

/**
 * @template {keyof CallbacksMap} T
 * @param {T} eventType The identifier of the event type.
 * @param {function(CallbacksMap[T]) : void} cb The callback to invoke when the event occurs.
 */
addEventListener(eventType, cb) {
    // ...
}

The cleanest way to do this that I've found so far is using the Parameters type.
This allows you to declare functions like you usually do, like function functionName() {}, rather than var functionName = function() {}.
And on classes you can declare methods like normally rather than this.methodName = () => {}

/**
 * This function takes a number and a string.
 * @callback signatureA
 * @param {number} num
 * @param {string} str
 */

/**
 * This function takes a boolean and an object
 * @callback signatureB
 * @param {boolean} bool
 * @param {Object} obj
 */

/**
 * @param {Parameters<signatureA> | Parameters<signatureB>} args
 */
function overloaded(...args) {}

playground link

Unfortunately the function description doesn't get copied, but hopefully #46353 fixes that.

I recently tried to type the following templated overload with JSDoc, without success. Resorted to a .d.ts declaration and @type the export.

declare function processFoo<T>(foo: Foo, transform: (bar: BarOfFoo) => Promise<T>): Promise<T>;
declare function processFoo(foo: Foo): Promise<DefaultBarOfFoo>;

I recently tried to type the following templated overload with JSDoc, without success. Resorted to a .d.ts declaration and @type the export.

I have been forced to do the same. It seems that each typescript/tsserver update becomes stricter. Workaround which used to work, are no longer accepted.

Where possible I just use typescript natively now.

With a workaround it's still possible to do this with jsdoc only, but now we need to use type casting.
And with type casting it now automatically recognizes the types of the parameters:

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

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

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

class MonaLisa {
    monaLisa = /** @type {Gioconda} */(
        (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;

class BarOfFoo {}
class Foo {}
class DefaultBarOfFoo {}

/**
@typedef {{
    <T>(foo: Foo, transform: (bar: BarOfFoo) => Promise<T>): Promise<T>
    (foo: Foo): Promise<DefaultBarOfFoo>
}} ProcessFoo
*/

const processFoo = /** @type {ProcessFoo} */(
    async(foo, transform) => {
        if (transform)
            return await transform(foo);
        return new DefaultBarOfFoo;
    }
);

/** @param {BarOfFoo} bar */
async function transformNumber(bar) {
    return 1;
}

/** @param {BarOfFoo} bar */
async function transformString(bar) {
    return 'str';
}

/** @type {Promise<number>} */
const test1 = processFoo(new Foo, transformNumber);
/** @type {Promise<string>} */
const test2 = processFoo(new Foo, transformString);
/** @type {Promise<DefaultBarOfFoo>} */
const test3 = processFoo(new Foo);

Playground Link

/**
@typedef {{
    <T>(foo: Foo, transform: (bar: BarOfFoo) => Promise<T>): Promise<T>
    (foo: Foo): Promise<DefaultBarOfFoo>
}} ProcessFoo
*/

TIL you can write a function typedef this way in TS JSdoc! The type cast of the function was expected and I don't mind it in this case.

/**
@typedef {{
    <T>(foo: Foo, transform: (bar: BarOfFoo) => Promise<T>): Promise<T>
    (foo: Foo): Promise<DefaultBarOfFoo>
}} ProcessFoo
*/

Ouch! My brain.

Is there a reference which explains this syntax?

I found https://austingil.com/typescript-function-overloads-with-jsdoc/ which talk about it, but nothing that explains it, especially how the template works in this context.

Inside the {...} in jsdoc we can use any TypeScript syntax. And in TypeScript this overload with a generic would look like this:

class BarOfFoo {}
class Foo {}
class DefaultBarOfFoo {}

function processFoo<T>(foo: Foo, transform: (bar: BarOfFoo) => Promise<T>): Promise<T>
function processFoo(foo: Foo): Promise<DefaultBarOfFoo>
function processFoo<T>(foo: Foo, transform?: any) : any {
}

let asType: typeof processFoo;

Playground Link

Now just place your mouse over "asType" and you get the tooltip:

let asType: {
    <T>(foo: Foo, transform: (bar: BarOfFoo) => Promise<T>): Promise<T>;
    (foo: Foo): Promise<DefaultBarOfFoo>;
}

I have came across the following (a little bit hacky) idea:

/**
 * @param {number} x
 * @returns {'number'}
 */
function typeName__1(x) {};

/**
 * @param {string} x
 * @returns {'string'}
 */
function typeName__2(x) {};

/**
 * @param {unknown} x
 * @returns {string}
 */
function typeName(x) {
  return typeof x;
}

Now if we modify the parser slightly and we teach it to interpret [name]__[number] as a body-less function declaration, it seems to work quite nicely. It only requires a couple lines of code to be changed in parseFunctionDeclaration(), i.e. something along these lines:

            let node: FunctionDeclaration;
            const match = name && /(.*)__\d+$/.exec(name.escapedText.toString());
            if (match) {
                const newName = factory.createIdentifier(match[1]);
                node = factory.createFunctionDeclaration(decorators, modifiers, asteriskToken, newName, typeParameters, parameters, type, undefined);
            }
            else {
                node = factory.createFunctionDeclaration(decorators, modifiers, asteriskToken, name, typeParameters, parameters, type, body);
            }

I tested it locally and it works quite well.

Of course this is not backwards compatible, so perhaps we could also decorate that declaration with additional JSDoc tag to control this behavior in a more granular way, e.g.

/**
 * @param {number} x
 * @returns {'number'}
 */
function /** @name typeName */ typeName__1(x) {};

This approach has a couple of nice properties:

  1. From TS perspective symbols for typeName__1 and typeName__2 are not really declared so if someone tries to use them, e.g. typeName__1(123), this will actually produce an error, which is desirable.
  2. From JS perspective these are just no-op functions which can be safely ignored.
  3. The same technique should work for method declarations.

Successful overload by following this issue
#55056

/**
 * @see {@link GroupBy}
 * @template T
 * @template K
 * @overload
 * @param {T[]} items
 * @param {(item: T) => K} callback
 * @returns {Record<K, T>}
 */

/**
 * @see {@link GroupBy}
 * @template T
 * @template K
 * @overload
 * @param {T[]} items
 * @param {(item: T) => K} callback
 * @param {boolean} isMultiRow
 * @returns {Record<K, T[]>}
 */

function GroupBy(items, callback, isMultiRow) {
  if (!callback) throw new Error('GroupBy: callback is required')
  const obj = {}
  if (!items.length) return obj
  const mode = isMultiRow ? 'multi' : 'single'
  for (let i = 0; i < items.length; i++) {
    const key = callback(items[i], i)
    switch (mode) {
      case 'multi':
        if (!obj[key]) obj[key] = []
        obj[key].push(items[i])
        break
      case 'single':
        if (!obj[key]) obj[key] = items[i]
        break
    }
  }
  return obj
}

const items = [
  { id: 'foo', name: 'foo' },
  { id: 'bar', name: 'bar' },
]

const gAryB = GroupBy(items, item => item.id, true)
const gObjB = GroupBy(items, item => item.id)
console.log(gAryB[''][0].id)
console.log(gObjB[''].id)

image
image
image
image
image