question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

A way to specify class properties using JSDoc

See original GitHub issue

Search Terms

JSDoc class properties any key @property

Suggestion

I would like to be able to extend the type of an ES6 class using JSDoc comments, such that TypeScript understands there are other properties that might be added to the object later.

Alternatively, I would like a way to indicate that a given class acts as a container that can contain any possible key.

Use Cases

I have a class that is used like a DTO. It has some properties that are guaranteed to be there, but is also used for a lot of optional properties. Take, for example:

class DTO {
    constructor(id) {
         /**
          * @type {string}
          */
         this.id = id;
    }
}

TypeScript now recognizes the object type, but whenever I try to use other properties, it complains. Currently, I’m resorting to something like this as a work-around:

class DTO {
    constructor(id) {
         /**
          * @type {string}
          */
         this.id = id;

         /**
          * @type {Object?}
          */
         this.otherProperty;
    }
}

But it’s ugly and verbose, and worse, it includes actual JavaScript code, that serves no purpose other than to provide type information.

Examples

Rather, what I would like to do is something like this (that I would like to be equivalent to the snippet above):

/**
 * @property {Object} [otherProperty]
 */
class DTO {
    constructor(id) {
         /**
          * @type {string}
          */
         this.id = id;
    }
}

Another equivalent alternative could be (but currently doesn’t compile because of a “Duplicate identifier” error):

/**
 * @typedef {Object} DTO
 * @property {string} id
 * @property {Object} [otherProperty]
 */
class DTO {
    constructor(id) {
         this.id = id;
    }
}

Or, otherwise, some way to indicate this class can be extended with anything. Which means I would like a way to specify the following TypeScript using JSDoc and I want it to apply to an existing ES6 class (because I do use the class to be able to do instanceof checks):

interface DTO {
    id: string,
    [key: string]: any,
}

Checklist

My suggestion meets these guidelines:

  • This wouldn’t be a breaking change in existing TypeScript / JavaScript code
  • This wouldn’t change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn’t a runtime feature (e.g. new expression-level syntax)

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:32
  • Comments:21 (7 by maintainers)

github_iconTop GitHub Comments

7reactions
miyasudokorocommented, Jul 29, 2020

Here is my use case, which involves somersaults through the hoops of making good JSDocs for Proxy declarations in pure JavaScript.

The problem here is that I need the following things to work simultaneously:

  1. @param {MyClass} for JSDoc
  2. @param {MyClass} for TypeScript
  3. instanceof MyClass
  4. Istanbul code coverage

Class constructor version

/** @class MyClass
 * @property email {string} The user's email
 * @property name {string} The user's name
 */
export default class MyClass {
    constructor( target ) {
        return new Proxy( target, proxyHandler ); // you can use "new MyClass"
    }
}
const proxyHandler = {
    // lots of stuff here
    getPrototypeOf: () => MyClass.prototype // you can use "instanceof MyClass"
}

// ... later in another file ...
import MyClass from './MyClass.js';

/** @param {MyClass|OtherClass} the data
 */
getUserEmail( myInstance ) {
    if ( myInstance instanceof MyClass ) {
        return myInstance.email;
    }
}

Factory version using empty class

/** @class MyClass
 * @property email {string} The user's email
 * @property name {string} The user's name
 */
export default function MyClass() {} // no "new MyClass" in this implementation

/** Factory for MyClass proxy.
 * @returns {MyClass}
 */
MyClass.Factory = function( target ) {
    return new Proxy( target, proxyHandler );
}
const proxyHandler = {
    // lots of stuff here
    getPrototypeOf: () => MyClass.prototype // you can use "instanceof MyClass"
}

// the use in the other file is same as class constructor version

Factory version using @typedef

/** @typedef MyClass
 * @property email {string} The user's email
 * @property name {string} The user's name
 */

const proxies = new WeakSet();

/** Factory for MyClass proxy.
 * @returns {MyClass}
 */
export function MyClassFactory( target ) {
    const proxy = new Proxy( target, proxyHandler );
    proxies.add( proxy );
    return proxy;
}
const proxyHandler = {
    // lots of stuff here, but no getPrototypeOf trap
}

/** Whether it's a MyClass instance.
 * @returns {boolean}
 */
export function isInstanceOfMyClass( proxy ) {
    return proxies.has( proxy );
}

// ... later in another file ...
import MyClass from './MyClass.js';

/** @param {MyClass|OtherClass} the data
 */
getUserEmail( myInstance ) {
    if ( isInstanceOfMyClass( myInstance ) ) {  // and here is where we really confuse my future colleagues
        return myInstance.email;
    }
}

Why I don’t like the workarounds

  1. The @typedef implementation above shows the kind of weirdness that happens if you need to use the functionality of instanceof with a dummy class. The entire implementation works fine, but reinventing the instanceof wheel sort of jumps off the future code maintenance cliff. Future junior programmers are not going to understand why the function isInstanceOfMyClass even exists, let alone go looking for it to use it until their code blows up.
  2. The original poster’s workaround involves declaring properties on the dummy MyClass. I have implemented this workaround at this time, and it comes out to 90 lines of code/JSDocs for what should be 22 lines of JSDocs. However, none of any declared properties of MyClass would be actually reachable code, because the proxy is the true handler of every property. That means Istanbul code coverage fails for all the non-ugly implementations of the workaround. The only way to get it to work is to declare the properties inside the class constructor before you return the proxy. It’s messy and, code-wise, pointless.
  3. If I make a d.ts file for this, it will be the only d.ts file in the whole (small) project, because I don’t need any for anything else. I don’t want to make exactly one d.ts file sitting inside an otherwise pure-JavaScript project solely to declare 22 properties this one time. Besides that, I would need to maintain the JSDoc @property statements anyway so that the JSDocs come out correctly. Documenting things twice is not good.
  4. This whole situation is obviously an oversight on your part. JSDoc has supported @property since its inception. It’s not like JSDoc added some new feature and you haven’t caught up yet. This should have been one of the first pieces of JSDoc-to-TypeScript support. Plus, you already support @property for @typedef, so there can’t be that much extra code needed.
3reactions
trusktrcommented, Feb 15, 2022

Note that this syntax,

class DTO {
	/**
	 * @type {string | undefined}
	 */
	other;
}

introduces runtime behavior that will break code because the other field gets defined in constructor with an undefined value that can shadow anything a super() call may have put in place.

Here’s an example of how it breaks. This is the plain JS code:

class Base {
  constructor() {
    this.init()
  }
}

class Subclass extends Base {
  init() {
    this.other = 123
  }
}

console.log(new Subclass().other) // logs "123"

Now we try to add types with JSDoc and it breaks:

class Base {
  constructor() {
    this.init()
  }
}

class Subclass extends Base {
  /** @type {number} */
  other;

  init() {
    this.other = 123
  }
}

console.log(new Subclass().other) // logs "undefined"

We need the equivalent of declare other: number in TS for plain JS / JSDoc.

I understand we can refactor code to make it work, but in some cases that may be a lot more work, especially in rather large plain JS projects.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to use JSDoc to document an ES6 class property
Move the JSDoc for someProperty into the constructor where it is first ... Another way is to declare them in class documentation as...
Read more >
Use JSDoc: @property
The @property tag is a way to easily document a list of static properties of a class, namespace or other object. Normally JSDoc...
Read more >
Short-jsdoc User Guide
short-jsdoc supports class inheritance. Only single class inheritance is supported, this is, a class can only extend only other one class. Also, javascript....
Read more >
How to comment your JavaScript code with JSDoc
TOC · What is JSDoc? · How to document an element · Documenting a variable · Documenting parameters · Object parameters · Documenting...
Read more >
How to document object properties with JSDoc
How to document object properties with JSDoc ; function getWizardMessage (wizard) ; let wizard = { name ; /** * Get the details...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found