Search code examples
typescriptvisual-studio-codeintellisense

Why is the style property suggested by TypeScript for the result of document.getElementById(), but not document.querySelector()?


When I write "document.querySelector()." and click CTRL+Spacebar to trigger suggestions, the style property does not get suggested, but it seems to work fine with document.getElementById().

Why is this happening and how do I fix this?

With querySelector:

With getElementById:

I expected the IntelliSense to suggest the 'style' property with the 'document.querySelector().' but it did not. Why?


Solution

  • If you're in a hurry and don't care about the "why", skip to the end of this post for the "solution"

    This is interesting. According to the DOM standard, both querySelector and getElementById return either an Element instance, or null.

    Docs:

    However, in lib.dom.ts, querySelector is typed to return Element | null, and getElementById for HTML documents is typed to return HTMLElement | null. And the style DOM property is defined on HTMLElement, but not on Element. There some nuance in that the typings for querySelector also have overloads that detect selectors that are simply standard tag names in order to specialize those cases and return the corresponding type.

    This was already brought up in the TypeScript GitHub repo's issue tracker here: Document.getElementById() must return Element, not HTMLElement #19549, which was closed to move the discussion of that issue to Node.parentElement should be Element, not HTMLElement #4689. You can follow the discussion there. Here are some quotes of the maintainers' thoughts on the matter:

    This is arguable, because there was complainant before saying that it was too cumbersome to always cast the type Element to HTMLElement when in most common cases the actual type is HTMLElement, even though the spec says it should be Element.

    For example, the return type of getElementById is defined as Element, however we made it HTMLElement to avoid too much casting. If the type is some other Element, you can just cast it to Element first and then cast it again to the final type. I think in most cases the parentElement is HTMLElement too, therefore it might be better just leave it the way it is.

    - zhengbli

    @jun-sheaf sent a PR for these changes and I've been reviewing about whether it should make it in for 4.1.

    I don't think we should make these changes, because it's going to add break a lot of code in a way that people won't think is to their advantage. TS tries to balance correctness and productivity and I think making getElementById start returning code which needs to be casted to get the same tooling support we have today in the majority of cases (e.g. HTML nodes in a JS context) is going to be a bad call for TypeScript.

    [...]

    Switching it to getElementById<E extends Element = HTMLElement>(elementId: string): E | null on the other hand still allows for setting the return type less drastic ways for the uncommon cases:

    const logo2 = getElementById2("my-logo") as SVGPathElement
    const logo3 = getElementById2<SVGPathElement>("my-logo")
    

    But doesn't hinder the default JS tooling support.

    - orta

    That issue was closed as completed by this commit: d561e08, which basically did what orta was talking about for getElementById. I.e. it still returns HTMLElement for HTML documents (but not other types of documents like SVG or XML) if I understand correctly.

    In other words, things are functioning as the TypeScript maintainers have written them to function.

    If you want the style property to available on the result of querySelector, then type-cast it to an HTMLElement using a type assertion (with as HTMLElement), or with a JSDoc annotation (with /**@type{HTMLElement}*/const e = document.querySelector(...); e.style;), or do a runtime type check with if (foo instanceof HTML...Element) {...}, or use the generic parameter of querySelector.