TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Support for declaring index signatures using JSDoc

Open handlebauer opened this issue 3 years ago • 7 comments

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?

handlebauer avatar Mar 02 '22 23:03 handlebauer

@sandersn is this possible?

RyanCavanaugh avatar Mar 04 '22 21:03 RyanCavanaugh

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.

sandersn avatar Mar 05 '22 01:03 sandersn

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
		}
	}
}

Gerrit0 avatar Apr 19 '22 16:04 Gerrit0

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
}

sandersn avatar Apr 28 '22 17:04 sandersn

any updates on this?

victoriomolina avatar Jan 11 '24 17:01 victoriomolina

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

webstrand avatar Jan 12 '24 15:01 webstrand

I figured out how to do it this in most scenarios, using @webstrand's example as a springboard. playground

SimplyLinn avatar Mar 12 '25 17:03 SimplyLinn