231

I'm trying to do this:

var script:HTMLScriptElement = document.getElementsByName("script")[0];
alert(script.type);

but it's giving me an error:

Cannot convert 'Node' to 'HTMLScriptElement': Type 'Node' is missing property 'defer' from type 'HTMLScriptElement'
(elementName: string) => NodeList

I can't access the 'type' member of the script element unless I cast it to the correct type, but I don't know how to do this. I searched the docs & samples, but I couldn't find anything.

Spongman
  • 9,665
  • 8
  • 39
  • 58

14 Answers14

285

TypeScript uses '<>' to surround casts, so the above becomes:

var script = <HTMLScriptElement>document.getElementsByName("script")[0];

However, unfortunately you cannot do:

var script = (<HTMLScriptElement[]>document.getElementsByName(id))[0];

You get the error

Cannot convert 'NodeList' to 'HTMLScriptElement[]'

But you can do :

(<HTMLScriptElement[]><any>document.getElementsByName(id))[0];
basarat
  • 261,912
  • 58
  • 460
  • 511
Spongman
  • 9,665
  • 8
  • 39
  • 58
  • i think they should look into this further, suppose you use $('[type:input]').each( function(index,element) and you need element to be cast to HTMLInputElement or HTMLSelectElement depending on which property you need to set/get, casting use (element).selectedIndex=0; adds () around element , kind of ugly – rekna Oct 05 '12 at 17:35
  • +1 that answered my question http://stackoverflow.com/questions/13669404/typescript-problems-with-type-system – lhk Dec 02 '12 at 16:18
  • In the long run (after 0.9 is out) you should be able to cast it to something like NodeList, plus getElementsByName will be able to use string literal type overrides to get this right without any casting at all! – Peter Burns Apr 07 '13 at 00:11
  • any is just huge, without ts would just be a joke – Luke Sep 18 '13 at 19:09
  • 6
    after 1.0, the syntax should be ```(>document.getElementsByName(id))[0];``` – Will Huang Jul 10 '14 at 12:42
  • `any` is the bridge, but it shouldn't be applied here. `NodeList` is not an array of `Node`, and shouldn't be treated as an array. A better way would be `Array.prototype.slice.call(nodeList);` – vilicvane Nov 15 '14 at 06:43
  • Thank you. I'm sad that it has to be done like this, but it works. Better than nothing:/ – Laker Jun 21 '16 at 20:12
  • 3
    You can also use as to cast. var script = document.getElementsByName("script")[0] as HTMLScriptElement; – JGFMK Sep 02 '17 at 15:13
  • The recommended type to use is `HTMLInputElement` according to https://github.com/Microsoft/TypeScript/issues/10453. – Daedalon Mar 21 '18 at 15:02
49

Do not type cast. Never. Use type guards:

const e = document.getElementsByName("script")[0];
if (!(e instanceof HTMLScriptElement)) 
  throw new Error(`Expected e to be an HTMLScriptElement, was ${e && e.constructor && e.constructor.name || e}`);
// locally TypeScript now types e as an HTMLScriptElement, same as if you casted it.

Let the compiler do the work for you and get errors when your assumptions turn out wrong.

It may look overkill in this case, but it will help you a lot if you come back later and change the selector, like adding a class that is missing in the dom, for example.

Benoit B.
  • 3,561
  • 24
  • 19
  • 2
    It would seem safe here, surely? We can guarantee that e is always going to be an instance of HTMLScriptElement, can't we (unless it doesn't exist, I suppose)? – Richard Hunter Sep 30 '20 at 15:11
  • I couldn't seem to get any type casting to work but this worked. – Souleste Aug 26 '21 at 22:58
  • Casting is a sign you cannot programatically assert a type. In this case, you can programatically assert a type, so you should not cast. If you cast, you have a leaky type system and it will break eventually. – WoodyWoodsta Jun 02 '23 at 15:54
  • I have found that TS does not recognize the type in a nested `querySelector` (e.g. `document.querySelector("p > a")`) in which case angle bracket type assertions are acceptable. – Elijah Mock Jul 20 '23 at 21:06
36

As of TypeScript 0.9 the lib.d.ts file uses specialized overload signatures that return the correct types for calls to getElementsByTagName.

This means you no longer need to use type assertions to change the type:

// No type assertions needed
var script: HTMLScriptElement = document.getElementsByTagName('script')[0];
alert(script.type);
Fenton
  • 241,084
  • 71
  • 387
  • 401
  • how do you do it in object notation? ie I can't do **{name: : document.querySelector('#app-form [name]').value,}** – Nikos Jan 14 '17 at 10:57
  • 3
    this worked: name: ( document.querySelector('#app-form [name]')).value, – Nikos Jan 14 '17 at 11:06
21

You always can hack type system using:

var script = (<HTMLScriptElement[]><any>document.getElementsByName(id))[0];
Jack128
  • 1,113
  • 10
  • 15
17

We could type our variable with an explicit return type:

const script: HTMLScriptElement = document.getElementsByName(id).item(0);

Or assert as (needed with TSX):

const script = document.getElementsByName(id).item(0) as HTMLScriptElement;

Or in simpler cases assert with angle-bracket syntax.


A type assertion is like a type cast in other languages, but performs no special checking or restructuring of data. It has no runtime impact, and is used purely by the compiler.

Documentation:

TypeScript - Basic Types - Type assertions

Leo
  • 10,407
  • 3
  • 45
  • 62
  • 1
    Tks! the second option work for me. The first option my lint print: Type 'HTMLElement | null' is not assignable to type 'HTMLScriptElement'.   Type 'null' is not assignable to type 'HTMLScriptElement'. (is my first project em Typescript :S hahaha) – LuanLuanLuan Nov 17 '21 at 15:10
13

To end up with:

  • an actual Array object (not a NodeList dressed up as an Array)
  • a list that is guaranteed to only include HTMLElements, not Nodes force-casted to HTMLElements
  • a warm fuzzy feeling to do The Right Thing

Try this:

let nodeList : NodeList = document.getElementsByTagName('script');
let elementList : Array<HTMLElement> = [];

if (nodeList) {
    for (let i = 0; i < nodeList.length; i++) {
        let node : Node = nodeList[i];

        // Make sure it's really an Element
        if (node.nodeType == Node.ELEMENT_NODE) {
            elementList.push(node as HTMLElement);
        }
    }
}

Enjoy.

Johannes Fahrenkrug
  • 42,912
  • 19
  • 126
  • 165
13

Rather than using a type assertion, type guard, or any to work around the issue, a more elegant solution would be to use generics to indicate the type of element you're selecting.

Unfortunately, getElementsByName is not generic, but querySelector and querySelectorAll are. (querySelector and querySelectorAll are also far more flexible, and so might be preferable in most cases.)

If you pass a tag name alone into querySelector or querySelectorAll, it will automatically be typed properly due to the following line in lib.dom.d.ts:

querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;

For example, to select the first script tag on the page, as in your question, you can do:

const script = document.querySelector('script')!;

And that's it - TypeScript can now infer that script is now an HTMLScriptElement.

Use querySelector when you need to select a single element. If you need to select multiple elements, use querySelectorAll. For example:

document.querySelectorAll('script')

results in a type of NodeListOf<HTMLScriptElement>.

If you need a more complicated selector, you can pass a type parameter to indicate the type of the element you're going to select. For example:

const ageInput = document.querySelector<HTMLInputElement>('form input[name="age"]')!;

results in ageInput being typed as an HTMLInputElement.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • But how is this any better than a normal type assertion? It's just a more obfuscated form of type assertion. – chpio Nov 04 '22 at 11:59
  • Type *assertions* are inherently unsafe. TS will happily let you do `const foo = {} as HTMLElement; foo.style.background = 'green';` and a runtime error will result. No matter the situation, utilizing generics is a much better choice when possible because they don't allow you to use *just anything* - they'll only allow you to use a type that the function permits. Generics are a core part of the type system and are used everywhere - I don't think calling them an obfuscation is fair. – CertainPerformance Nov 04 '22 at 13:06
  • 1
    Adding sensible constraints to how a value or function can be used and working within those constraints is one of the larger benefits of TypeScript. You don't want to do `const fn = (num: any) => {` - you want `const fn = (num: number) => {` to be precise and prevent yourself from making mistakes. For similar reasons, you should use generics instead of `as`. – CertainPerformance Nov 04 '22 at 13:13
  • Yes, it is constrained. And it's still just a hidden type assert, and still as unsafe as a normal assert. There's nothing preventing you from using a wrong type as a generic, then accessing a non existing property, resulting in a runtime error. – chpio Nov 04 '22 at 13:19
  • Also i haven't called generics an obfuscation, i called your use of it in this situation an obfuscated type assert. – chpio Nov 04 '22 at 13:20
9

Just to clarify, this is correct.

Cannot convert 'NodeList' to 'HTMLScriptElement[]'

as a NodeList is not an actual array (e.g. it doesn't contain .forEach, .slice, .push, etc...).

Thus if it did convert to HTMLScriptElement[] in the type system, you'd get no type errors if you tried to call Array.prototype members on it at compile time, but it would fail at run time.

DhruvJoshi
  • 17,041
  • 6
  • 41
  • 60
Bill Ticehurst
  • 1,728
  • 12
  • 14
  • 1
    granted that is correct, however not entirely useful. the alternative is to go via 'any' which provides no useful type checking whatsoever... – Spongman Oct 02 '12 at 19:49
3

This seems to solve the problem, using the [index: TYPE] array access type, cheers.

interface ScriptNodeList extends NodeList {
    [index: number]: HTMLScriptElement;
}

var script = ( <ScriptNodeList>document.getElementsByName('foo') )[0];
Tobias Cudnik
  • 9,240
  • 4
  • 24
  • 18
2

I would also recommend the sitepen guides

https://www.sitepen.com/blog/2013/12/31/definitive-guide-to-typescript/ (see below) and https://www.sitepen.com/blog/2014/08/22/advanced-typescript-concepts-classes-types/

TypeScript also allows you to specify different return types when an exact string is provided as an argument to a function. For example, TypeScript’s ambient declaration for the DOM’s createElement method looks like this:

createElement(tagName: 'a'): HTMLAnchorElement;
createElement(tagName: 'abbr'): HTMLElement;
createElement(tagName: 'address'): HTMLElement;
createElement(tagName: 'area'): HTMLAreaElement;
// ... etc.
createElement(tagName: string): HTMLElement;

This means, in TypeScript, when you call e.g. document.createElement('video'), TypeScript knows the return value is an HTMLVideoElement and will be able to ensure you are interacting correctly with the DOM Video API without any need to type assert.

sebilasse
  • 4,278
  • 2
  • 37
  • 36
1

Could be solved in the declaration file (lib.d.ts) if TypeScript would define HTMLCollection instead of NodeList as a return type.

DOM4 also specifies this as the correct return type, but older DOM specifications are less clear.

See also http://typescript.codeplex.com/workitem/252

Peter
  • 19
  • 1
  • 1
1

As an extension of CertainPerformance's answer, if you utilize declaration merging to augment the standard definition library's Document interface, you can add a generic override for the getElementsByName method (or for any other for that matter) with parameter default set to HTMLElement to mimic the behaviour of the non-generic version when the type argument is not provided explicitly:

interface Document
  extends Node,
    DocumentAndElementEventHandlers,
    DocumentOrShadowRoot,
    GlobalEventHandlers,
    NonElementParentNode,
    ParentNode,
    XPathEvaluatorBase {
  getElementsByName<T extends HTMLElement>(elementName: string) : NodeListOf<T>;
}

Then in the user code you can explicitly pass the desired type:

const scripts = document.getElementsByName<HTMLScriptElement>("name"); //NodeListOf<HTMLScriptElement>

Playground


Note that you need to respecify the extends list because only identical declarations can be merged.

0

Since it's a NodeList, not an Array, you shouldn't really be using brackets or casting to Array. The property way to get the first node is:

document.getElementsByName(id).item(0)

You can just cast that:

var script = <HTMLScriptElement> document.getElementsByName(id).item(0)

Or, extend NodeList:

interface HTMLScriptElementNodeList extends NodeList
{
    item(index: number): HTMLScriptElement;
}
var scripts = <HTMLScriptElementNodeList> document.getElementsByName('script'),
    script = scripts.item(0);
Mike Keesey
  • 156
  • 1
  • 6
0
var script = (<HTMLScriptElement[]><any>document.getElementsByName(id))[0];