10

I have a JS file with a // @ts-check directive, using JSDoc comments to denote types.

The problem is that the elements fail type checks when retrieved from the document.

So we have in HTML:

<input id="myInput">...

When I get this element in JS the type checking throws an error:

// @ts-check
const myInput = document.getElementById('myInput');
myInput.value = 'foobar';

Property 'value' does not exist on type 'HTMLElement'

If I specify the expected type with a JSDoc @type comment then that also fires an error:

// @ts-check

/** @type {HTMLInputElement} */
const myInput = document.getElementById('myInput');
myInput.value = 'foobar';

Type 'HTMLElement' is not assignable to type 'HTMLInputElement'.
Property 'accept' is missing in type 'HTMLElement'.

If I was in TS, I could use document.getElementById('myInput') as HTMLInputElement to tell it that I expect this type.

How do I do this in JS with @ts-check?

Keith
  • 150,284
  • 78
  • 298
  • 434

3 Answers3

15

The fix for this is to put the @type declaration between the variable and the retrieval, and adding ().

Like this:

// @ts-check

const myInput = /** @type {HTMLInputElement} */ (document.getElementById('myInput'));
myInput.value = 'foobar';

This syntax is fairly clunky and horrible, but they've closed the bug so I guess the above syntax is the official way to handle this.

Keith
  • 150,284
  • 78
  • 298
  • 434
2

You can use a runtime check if you want to be absolutely sure, this also persuades typescript that the assignment is safe.

const myInput = document.getElementById('myInput');
if (myInput instanceof HTMLInputElement) {
    myInput.value = 'foobar';
}
Tom Cumming
  • 1,078
  • 7
  • 9
  • Cheers, but in this context I specifically don't want a runtime check, as in the basically impossible case where `myInput` is not an `HTMLInputElement` I've got nothing to do instead except throw an exception. Belt and braces is fine, but I have loads of elements, which means loads of belts and loads of braces, which just feels like a waste of time. – Keith Aug 29 '18 at 11:46
0

The answer by @Keith is excellent, but it only covers individual Element nodes via:

  • @type {HTMLInputElement}

We also need to deal with HTMLCollections - which we can do via:

  • @type {HTMLCollectionOf<Element>}

Faced with a long file of javascript which may contain any number of Element and HTMLCollection captures, we can take advantage of the following two search and replace Regexes:

Find Element captures:

((let|const|var)\s([^\s=]+)\s?=)\s?(([^\.]+)\.(((get|query)[^\(]+\(([^\)]+\))\[\d+\])|(getElementById|querySelector)\(([^\)]+\))))

Replace with:

$1 /** @type {HTMLInputElement} */ ($4)

Then...

Find HTMLCollection captures:

((let|const|var)\s([^\s=]+)\s?=)\s?(document\.(get|query)[^\(]+\(([^\)]+\)))

Replace with:

$1 /** @type {HTMLCollectionOf<Element>} */ ($4)
Rounin
  • 27,134
  • 9
  • 83
  • 108