How to use JSDoc to add type contraints to generic function arguments

Today I learnt …

The other day, while working on a Typescript/Javascript codebase, I found myself writing a function that took a single argument. That argument might be one of two types, and the function would return an array of the same type. For example, if the function was passed a string, it would return an array of strings. But if the function was passed a number, it would return an array of numbers. I had most of the function written; all that was missing was the type information.

Adding the necessary types would have been no problem in Typescript, where a sprinkling of generics would provide the information needed. If we name the function foo and simplify the code it contained, we end up with something like this:

type Result<T> = T extends string ? string[] : number[];

function foo<T extends string | number>(bar: T): Result<T> {
  if (typeof bar === "string") {
    return ["a", "b"] as Result<T>;
  } else {
    return [1, 2] as Result<T>;
  }
}

With this code, when I pass a string Typescript would know the result is guaranteed to be an array of strings. And when I pass a number, Typescript would know the result would be an array of numbers.

const result1 = foo("Hello!"); // result1 is a string[]
const result2 = foo(1); // result2 is a number[]

The problem I had was that the function was in a Javascript file, not a Typescript file, and so I needed to type the function using Typescript’s flavour of JSDoc. While this syntax has a lot in common with Typescript, it’s all added above the function in a comment rather than being inline. How exactly should I structure that comment?

I completely failed to find the answer online and instead worked it out by trial and error. Delightfully, the answer ended up being more succinct than the Typescript version:

/**
 * @template {string | number} T
 *
 * @param {T} bar
 * @returns {T extends string ? string[] : number[]}
 */
function foo(bar) {
  if (typeof bar === "string") {
    return ["a", "b"];
  } else {
    return [1, 2];
  }
}

The constraint on the Typescript function — <T extends string | number> — was replaced by using a @template tag, the bar argument was typed using @param, and the return type was just declared directly rather than needing to use a defined type. That in turn removed the need to coerce the return values (return ... as Result<T>). All in all, quite an elegant solution.