Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Type Casting in JSDoc #25028

Closed
4 tasks done
rbiggs opened this issue Jun 17, 2018 · 7 comments
Closed
4 tasks done

Support for Type Casting in JSDoc #25028

rbiggs opened this issue Jun 17, 2018 · 7 comments

Comments

@rbiggs
Copy link

rbiggs commented Jun 17, 2018

Search Terms

jsdoc, type casting

Suggestion

Support for indicating type casting in JSDoc comments. Currently there is no supported way to do so, at least not in the documentation for support for JSDoc listed here: JSDoc support in JavaScript Type casting with parenthesis is supported in the Closure Compiler using parenthesis around the property (scroll to the bottom of the page): https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler

Use Cases

I use the support for type checking with JSDoc in JavaScript files in Visual Studio Code. My code deals with DOM nodes and DOM manipulation. I often run into situations where I get an object of type Node and then need to use it's HTMLElement interface to access methods such as setAttribute, etc. Because the type is Node, I get type warnings that the property doesn't exist on type Node. Since there is no way to cast from Node to HTMLElement, the only recourse is to escape the method with braces and quotes. This means my code is littered with escaped properties--string spaghetti with no meaning.

Examples

/**
 * @description Function to convert hyperscript/JSX into DOM nodes.
 * @param {string | number | Object} node A node to create. This may be a hyperscript function or a JSX tag which gets converted to hyperscript during transpilation.
 * @param {boolean} [isSVG] Whether the node is SVG or not.
 * @returns {Node} An element created from a virtual dom object.
 */
export function createElement(node, isSVG) {
// implementation details
}

The above code is used to create an HTML element. At some point it will be accessed by another function to add attributes and properties:

/**
 * @description Function to set properties and attributes on element.
 * @param {Node} element The element to set props on.
 * @param {string} prop The property/attribute.
 * @param {string | number | boolean} value The value of the prop.
 * @param {string | number | boolean} oldValue The original value of the prop.
 * @param {boolean} isSVG Whether this is SVG or not
 * @returns {void} undefined
 */
export function setProp(element, prop, value, oldValue, isSVG) {
//When setting attributes, need to escape to avoid type errors:
  if (prop === 'dangerouslysetinnerhtml') {
    element['innerHTML'] = value
  } else {
    element['setAttribute'](prop, value)
  }
  if (
      value == null ||
      value === 'null' ||
      value === 'undefined' ||
      value === 'false' ||
      value === 'no' ||
      value === 'off'
    ) {
      element['removeAttribute'](prop)
    }
}

What I would like to do is something like this to type cast Node to HTMLElement::

/**
 * @description Function to set properties and attributes on element.
 * @param {Node} element The element to set props on.
 * @param {string} prop The property/attribute.
 * @param {string | number | boolean} value The value of the prop.
 * @param {string | number | boolean} oldValue The original value of the prop.
 * @param {boolean} isSVG Whether this is SVG or not
 * @returns {void} undefined
 */
export function setProp(element, prop, value, oldValue, isSVG) {
//When setting attributes, need to escape to avoid type errors:
  if (prop === 'dangerouslysetinnerhtml') {
   // Cast Node to HTMLElement:
   /** @type {HTMLElement} (element) */
    element.innerHTML = value
  } else {
   // Cast Node to HTMLElement:
    /** @type {HTMLElement} (element) */
    element.setAttribute(prop, value)
  }
  if (
      value == null ||
      value === 'null' ||
      value === 'undefined' ||
      value === 'false' ||
      value === 'no' ||
      value === 'off'
    ) {
      // Cast Node to HTMLElement:
      /** @type {HTMLElement} (element) */
      element.removeAttribute(prop)
    }
}

The above is just one example of the problems I face daily due to lack of type casting with JSDoc when using Visual Studio Code with "javascript.implicitProjectConfig.checkJs": true setting in my preferences.

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)
@j-oliveras
Copy link
Contributor

You can already do it:

/**
 * @description Function to set properties and attributes on element.
 * @param {Node} element The element to set props on.
 * @param {string} prop The property/attribute.
 * @param {string | number | boolean} value The value of the prop.
 * @param {string | number | boolean} oldValue The original value of the prop.
 * @param {boolean} isSVG Whether this is SVG or not
 * @returns {void} undefined
 */
export function setProp(element, prop, value, oldValue, isSVG) {
//When setting attributes, need to escape to avoid type errors:
  if (prop === 'dangerouslysetinnerhtml') {
   // Cast Node to HTMLElement:
    (/** @type {HTMLElement} */ (element)).innerHTML = "" + value;
  } else {
   // Cast Node to HTMLElement:
    (/** @type {HTMLElement} */ (element)).setAttribute(prop, "" + value)
  }
  if (
      value == null ||
      value === 'null' ||
      value === 'undefined' ||
      value === 'false' ||
      value === 'no' ||
      value === 'off'
    ) {
      // Cast Node to HTMLElement:
      (/** @type {HTMLElement} */ (element)).removeAttribute(prop)
    }
}

Note: the two "" + value are because only string is valid.

@rbiggs
Copy link
Author

rbiggs commented Jun 17, 2018

Wow! That works perfectly. It's a slightly different syntax than Closure Compile, but I don't care. Is this documented somewhere? Also, since when has this been supported?

@j-oliveras
Copy link
Contributor

JSDoc support in JavaScript. For cast documentation see just before Patterns that are known NOT to be supported section.

Seems is supported since version 2.5.

@rbiggs
Copy link
Author

rbiggs commented Jun 17, 2018

Sheesh! I've looked at that document a million times. I have tried the casting technique mentioned there before but it never worked for me. Now I know why. I use PrettierJS and it strips the parens from the element cast because it sees them as unnecessary, causing the casting to fail. Guess I need to go over and log an issue with PrettierJS.

@rbiggs rbiggs closed this as completed Jun 17, 2018
@rbiggs
Copy link
Author

rbiggs commented Jun 17, 2018

By the way, I just noticed that this also allows you to handle expando properties:

/** @type {Object} element.vnode */ (element).vnode = node

Unfortunately, you can't be more explicit with the type. This only works if you cast the expando property to *, Object or any, which are all the same for TypeScript. At least you don't have to use braces and quotes around the property. The lesser of two evils.

@reinhrst
Copy link

The link to the JSDoc support in JavaScript is not working anymore (or at least the description is not on that page anymore).

For future reference this (very contrived) example:

/** @type {string} */
let x = /** @type string */ (3)

does the conversion (it took me a while to realise that the parentheses () around the 3 were necessary

Note that the code above still doesn't satisfy the Typescript linter though: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. (tsserver 2352), so in this case we would need one more step:

/** @type {string} */
let x = /** @type string */ (/** @type unknown */ (3))

@tbeseda
Copy link

tbeseda commented Aug 29, 2023

These examples were helpful and came up in early in my web searches. So I'd also add that you can line break if you prefer or if your formatter restricts line width.

To expand on the above example with a long line:

/** @type {string} */
const myString =
  /** @type {string} */
  (something.definitelyAString_butNotStrictlyTypedAsSuch)

This satisfies VS Code v1.81.1 built-in JS checking (setting js/ts.implicitProjectConfig.checkJs).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants