Support for declaring index signatures using JSDoc
Suggestion
🔍 Search Terms
TS in JS, JSDoc, index signatures, class
✅ Viability Checklist
- [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [x] This wouldn't change the runtime behavior of existing JavaScript code
- [x] This could be implemented without emitting different JS based on the types of the expressions
- [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- [x] This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
I've looked around for a while and I haven't found a means to declaring an index signature on an ES6 class using JSDoc.
Take this scenario:
class Cats {
/** @param {{ name: string, colour: string }[]} cats */
constructor(cats) {
for (let cat of cats) {
this[cat.name] = cat.colour
}
}
}
this[cat.name] complains:
Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'Cats'.
How do we solve this using JSDoc? I know this is a non-issue when employing different paradigms (e.g. cats.reduce((cats, cat) => ({ ...cats, [cat.name]: new Cat(cat) }), {})) but that's not always the way.
In TS, it is handled as so:
class Cats {
[name: string]: string
constructor(cats: { name: string, colour: string }[]) {
for (let cat of cats) {
this[cat.name] = cat.colour
}
}
}
📃 Motivating Example
Looking for the equivalent of a catch-all class-wide type declaration:
class Cats {
/** @type {{ [name: string]: string }} */
/** @param {{ name: string, colour: string }[]} cats */
constructor(cats) {
for (let cat of cats) {
this[cat.name] = cat.colour
}
}
}
Is implementing this possible? Or, perhaps, Is this achievable by some existing method that I missed?
@sandersn is this possible?
It is definitely possible, but this would be a new feature, so it needs a new tag, not @type. I don't even think there's an existing jsdoc tag that declares index signatures.
Edit: Any proposed tag needs to work on its own inside a class, as well as inside a @typedef.
Note: You can work around this on a per-method basis by declaring a this parameter. Relevant SO question
class Cats {
/**
* @this {{[k: string]: string}}
* @param {{ name: string, colour: string }[]} cats
*/
constructor(cats) {
for (let cat of cats) {
this[cat.name] = cat.colour
}
}
}
I don't know how much this feature makes sense. Index signature on classes are very constraining (more detail below), and all the other locations for index signatures that I can think of are in type positions, which are already possible to express in JSDoc. I think this needs better motivating examples.
Here are the examples of index signatures in classes that I can think of (thanks to @webstrand for suggesting the instanceof usage):
class Cats {
[s: string]: Cat
// only static methods are possible here because instance methods are not of type `Cat`
}
// more usually expressed as a module named 'cats.js'
export cats: Record<string, Cat>
// convert static methods to exported functions
// or a factory function
function makeCats() {
const cats: Record<string, Cat> = {}
return cats
// static functions go here
}
// but you can use `instanceof` on Cats, unlike the 'cats.js' module:
if (obj instanceof Cats) {
...
}
You could also add an index signature to any or unknown, but then the contents are untyped:
class Cats {
[s: string]: any
// methods are OK here
}
any updates on this?
With the advent of partial indexes, this might still have use? i.e.
namespace example {
class EventTarget {
[key: `on${string}`]: (event: Event) => void
}
class Foo extends EventTarget {
set onclick(value: (event: string) => void) {} // mistake prevented
}
}
playground for instance
I figured out how to do it this in most scenarios, using @webstrand's example as a springboard. playground