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

Replaces several DOM methods with weaker typings and closes microsoft/TypeScript#4689 #885

Merged
merged 8 commits into from
Dec 12, 2020

Conversation

jrandolf
Copy link
Contributor

@jrandolf jrandolf commented Jul 22, 2020

The following changes have been made:

…#4689

The following changes have been made:
* All occurences of `getElementById` takes an optional type parameter for manual typing.
* All occurences of `getElementsByClassName` takes an optional type parameter for manual typing.
* `parentElement` now types to `Element` (See microsoft#4689)
@jrandolf jrandolf requested a review from sandersn as a code owner July 22, 2020 21:22
@ghost
Copy link

ghost commented Jul 22, 2020

CLA assistant check
All CLA requirements met.

@HolgerJeromin
Copy link
Contributor

This will break many code parts.
Perhaps
getElementById<E extends Element = HTMLElement>(elementId: string): E | null;
will break less.

@jrandolf
Copy link
Contributor Author

jrandolf commented Jul 23, 2020

This will break many code parts.

I was thinking about this, but the spec states getElementById should return an Element. For example, SVGSVGElements. It is also simple to refactor this, given the fact we can now coerce the type via the type argument; a simple regular expression is all that is required. I wouldn't even mind developing a small CLI tool to refactor code toward this if really necessary.

IMO, if we continue to let wrong types live in this language, the future will be bleak as new interface may be impossible to implement. Considering getElementById should be used with manual typing to some extension of HTMLElement, this PR promotes a healthy dose of type-safety and readability.

@orta
Copy link
Contributor

orta commented Sep 10, 2020

Yeah, I agree with @HolgerJeromin - I think the move to generics is a good call for usability outside of the common HTML case, but I don't think it should switch to Element.

I'll write a longer post in the original TS issue but if this switches to getElementById<E extends Element = HTMLElement>(elementId: string): E | null; then I'm cool with getting this merged.

@orta
Copy link
Contributor

orta commented Sep 10, 2020

@jrandolf
Copy link
Contributor Author

jrandolf commented Oct 18, 2020

@orta Sorry for not getting back to you after a while. I've made the changes you proposed. Your arguments are reasonable, particularly because of the possibility of a 'strict-type' module if ever necessary.

However, I did not change the parentElement type back to HTMLElement. It's not uncommon to use parentElement to retrieve an SVGElement (e.g. in scripting animated/dynamic SVG content). Moreover, coercing parentElement to SVGElement using parentElement as unknown as SVGElement is just highly unnatural.

@HolgerJeromin
Copy link
Contributor

Perhaps the parent case can be more complex handled:

forreignObjectHTMLElement parent is svgSvgElement.
The rest HTMLElement parent have HTMLElement

SvgSvgElement can have svgSvgElement or HTMLElement as parent.
All the rest SvgElement have SvgElement as parents.

Or do i miss something?

@jrandolf
Copy link
Contributor Author

Sounds reasonable! I'll make these changes.

@jrandolf
Copy link
Contributor Author

jrandolf commented Oct 18, 2020

So the changes made so far:

  • All occurrences of getElementById take an optional type parameter for manual typing with default as HTMLElement.
  • All occurrences of getElementsByClassName take an optional type parameter for manual typing with default using case-by-case.
    • Element.getElementsByClassName returns HTMLElement by default.
    • SVGElement.getElementsByClassName returns SVGElement by default.
    • SVGForeignObjectElement.getElementsByClassName returns HTMLElement by default.
  • parentElement is case-by-case (See Node.parentElement should be Element, not HTMLElement TypeScript#4689)
    • Element.parentElement types to Element.
    • HTMLElement.parentElement types to HTMLElement.
    • SVGElement.parentElement types to SVGElement.

@@ -214,7 +214,7 @@ interface Node : EventTarget {
readonly attribute Document? ownerDocument;
Node getRootNode(optional GetRootNodeOptions options = {});
readonly attribute Node? parentNode;
readonly attribute Element? parentElement;
readonly attribute HTMLElement? parentElement;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a change from upstream?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this! Happened during a RegExp replace operation..

@HolgerJeromin
Copy link
Contributor

Can you add similar typings to querySelector, querySelectorAll and closest?

@jrandolf
Copy link
Contributor Author

Scope creep. There is already another discussion for that since the new template string types will change the playing field.

baselines/dom.generated.d.ts Outdated Show resolved Hide resolved
@orta
Copy link
Contributor

orta commented Dec 12, 2020

Thanks!

@orta
Copy link
Contributor

orta commented Jan 6, 2021

#970 reverts this PR - so that we can release 4.2 without this.

The revert isn't permanent, but it is too breaking to DefinitelyTyped and so it's likely to be very breaking to the rest of the TS/JS ecosystem and Anders argued that this technique is probably not what we want either way microsoft/TypeScript#42067 (comment)

It's going to need another look (and maybe we need to figure out a way to be able to determine breakability better inside PRs in this repo, because it takes a few steps to find out right now).

@felixfbecker
Copy link

It is very weird to me that the type parameter would default to HTMLElement.
document.querySelector('#id') is typed to return Element in dom.d.ts, why should document.getElementById('id') return HTMLElement by default (without any casts or type parameters from the user)? That seems incorrect and inconsistent...

I really like that the TypeScript DOM types are a trustworthy reference for how the types in the DOM actually are, often helping uncover and teaching cases that someone may not even know about (and much easier to read and more discoverable than the DOM spec). I think it would be a shame if TypeScript types moved away from modeling the correct types towards only modeling the majority-of-cases types.

It is actually very easy to accidentally write code like e.g.

document.getElementById(location.hash.slice(1))?.focus()

trusting TypeScript that it would give a type error if this was not correct and forgetting that a user-provided ID could very well match an SVG element, which would not have a .focus() method and result in a runtime Cannot call method 'focus' of undefined error. Imo, that is an error that should be caught by TypeScript, unless the user overrode it with a cast.

I understand the challenges with backwards compatibility, but as a user I'd rather update code to add casts to fix type errors. It wouldn't be the first type error in a dom.d.ts upgrade for more correct types 🙂

baselines/dom.generated.d.ts Show resolved Hide resolved
baselines/dom.generated.d.ts Show resolved Hide resolved
baselines/dom.generated.d.ts Show resolved Hide resolved
baselines/dom.generated.d.ts Show resolved Hide resolved
baselines/dom.generated.d.ts Show resolved Hide resolved
baselines/dom.generated.d.ts Show resolved Hide resolved
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

Successfully merging this pull request may close these issues.

5 participants