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

Typescript definitions model a private Dom interface instead of using "the" public definitions from lib.dom.d.ts #1227

Open
snuup opened this issue Jan 19, 2024 · 14 comments · Fixed by #1229
Assignees
Labels
bug Something isn't working

Comments

@snuup
Copy link

snuup commented Jan 19, 2024

Using happy-dom with typescript libraries is very hard, because happy-dom element types are unrelated to the types used by typed dom-libraries, which refer to the HTMLElement, Element, ... types as defined in lib.dom.d.ts .

happy-dom should expose its elements as those public types (lib.dom.d.ts) .

I might be able to do this or help.

@snuup snuup added the bug Something isn't working label Jan 19, 2024
@capricorn86
Copy link
Owner

capricorn86 commented Jan 19, 2024

We can look into using the types from lib.dom.d.ts, but it is not that easy.

The question is why you are mixing the types. Either you should use the Happy DOM Window as the global scope and then you can use lib.dom.d.ts, or if you want to create the Window where it is needed, it is probably better to not use lib.dom.d.ts as it will collide with the types from Happy DOM.

JSDOM doesn't have any types in the library, so my guess is that there are no types that can collide and Typescript will fallback on lib.dom.d.ts.

@valler
Copy link

valler commented Jan 21, 2024

It would be nice to have compatible interfaces.

JSDOM doesn't have any types in the library [...]

What exactly does this mean? Are the following patterns discouraged?

import type { HTMLAudioElement } from "happy-dom";
let el: ReturnType<document.createElement>;
let link: typeof import("happy-dom").HTMLLinkElement;

I am asking because I tried to get <HTMLAnchorElement> which apperently is burried somewhere deep in the lib.

Compatibility in terms of call signatures would also be nice. For example, generic type arguments and type inference for querySelector:

// How should this be done when working with Happy DOM?
const anchor = d.querySelector("a");
const anchor = d.querySelector<HTMLAnchorElement>(".class-name");

Currently the following is what seems to be working in Happy DOM?

// TS
const audio = document.querySelector("audio") as unknown as HTMLAudioElement | null;
// JS
const audio = /** @type {HTMLAudioElement | null} */ (/** @type {unknown} */(document.querySelector("audio")));

Is this how it is supposed to work? It feels like, in a typed environment, one has to go through quite some effort to let the type system know that a query returns a clickable element (or null).

@capricorn86
Copy link
Owner

capricorn86 commented Jan 21, 2024

It seems like HTMLAnchorElement and its interface IHTMLAnchorElement is missing from index.ts.

What I meant is that if you import Window and create an instance of it like this:

import { Window } from 'happy-dom';

const window = new Window();

Then it is not really correct to use lib.dom.d.ts as lib.dom.d.ts defines the types on the global scope. It will for example consider globalThis.document.querySelector('a') as valid, but in reality globalThis.document is undefined.

If Window is the global scope on the other hand. Either by running Javascript in the Window VM or by defining all properties on the global scope (e.g. by using @happy-dom/global-registrator), then it makes sense to use lib.dom.d.ts, and then the Happy DOM types should not be necessary.

If you use both types they can collide when a variable name matches the global type (e.g. using the variable name document and then access properties on it).

If you know a way which Happy DOM can import specific types from lib.dom.d.ts without importing all types from the type definition, please let me know. There has been some attempts in the past. It would be great if Happy DOM could leverage those types directly where it is possible.

@capricorn86 capricorn86 self-assigned this Jan 21, 2024
capricorn86 added a commit that referenced this issue Jan 21, 2024
…"index.js", so that they are easier to import. The missing elements are HTMLAnchorElement, HTMLButtonElement, HTMLOptGroupElement, HTMLOptionElement, HTMLUnknownElement and HTMLSelectElement. Non-implemented element classes are now exporting HTMLElement as its name, so that is is also possible to import them.
capricorn86 added a commit that referenced this issue Jan 21, 2024
…tions-model-a-private-dom-interface-instead-of-using-the-public-definitions-from-libdomdts

#1227@patch: Adds missing element classes and types to the export in …
@capricorn86 capricorn86 reopened this Jan 21, 2024
@capricorn86
Copy link
Owner

@valler HTMLAnchorElement, together with some other element classes that where missing in the export has been added now 🙂

You can read more about the release here:
https://github.com/capricorn86/happy-dom/releases/tag/v13.2.1

@valler
Copy link

valler commented Jan 21, 2024

@capricorn86 amazing 🙂

About the follwoing:

If you know a way which Happy DOM can import specific types from lib.dom.d.ts without importing all types from the type definition, please let me know. There has been some attempts in the past. It would be great if Happy DOM could leverage those types directly where it is possible.

That would be quite an undertaking? 🙄 Since TS is structural I'd guess one pretty much would have to work one's way up from the very bottom or look out for oppotunities to take some shortcuts, like extends, satisfies and generics. Importing specific types by itself is not really the problem, but having a matching implementation is, right?

Btw. I agree that mixing types is a bad idea or might cause problems. I think OP didn't raise the issue because the goal is to mix interfaces, but because there should be one interface definition for the DOM (lib.dom.d.ts), albeit having the option to choose between different implementations of that same interface (e.g. importing Happy DOM vs jsdom).

@capricorn86
Copy link
Owner

capricorn86 commented Jan 21, 2024

@capricorn86 amazing 🙂

About the follwoing:

If you know a way which Happy DOM can import specific types from lib.dom.d.ts without importing all types from the type definition, please let me know. There has been some attempts in the past. It would be great if Happy DOM could leverage those types directly where it is possible.

That would be quite an undertaking? 🙄 Since TS is structural I'd guess one pretty much would have to work one's way up from the very bottom or look out for oppotunities to take some shortcuts, like extends, satisfies and generics. Importing specific types by itself is not really the problem, but having a matching implementation is, right?

Btw. I agree that mixing types is a bad idea or might cause problems. I think OP didn't raise the issue because the goal is to mix interfaces, but because there should be one interface definition for the DOM (lib.dom.d.ts), albeit having the option to choose between different implementations of that same interface (e.g. importing Happy DOM vs jsdom).

I believe that Happy DOM mostly matches lib.dom.d.ts when I looked at it. There are some properties that exist in Happy DOM that doesn't exist in lib.dom.d.ts (event properties like "oncompositionstart" and newer functionality such as "getInnerHTML()") when I looked at it a couple of weeks ago.

However, I think the biggest problem is that we can't import lib.dom.d.ts as it would define all DOM classes on the global scope. I think the only way to solve this is to extract the source code from Typescript and build the types.

This may not solve it if Happy DOM has newer functionality that doesn't exist in lib.dom.d.ts yet for example. Then it will still complain about the DOM element not being compatible.

If developers wish to use lib.dom.d.ts with Happy DOM the same way as JSDOM, they can achieve the same result by excluding the types from Happy DOM. It works with JSDOM as it doesn't have any types, while Happy DOM has.

@valler
Copy link

valler commented Jan 21, 2024

Here's the catch:

It works with JSDOM as it doesn't have any types, while Happy DOM has.

Having compatible interfaces essentially boils down to something like in the following toy example. The implementation of the interface in JS may differ, but the interface definition should not be rewritten to remain compatible:

// My Window for my own DOM
export class Window implements Window {} // Start implementing lib.dom.d.ts Window.

@capricorn86
Copy link
Owner

Here's the catch:

It works with JSDOM as it doesn't have any types, while Happy DOM has.

Having compatible interfaces essentially boils down to something like in the following toy example. The implementation of the interface in JS may differ, but the interface definition should not be rewritten to remain compatible:

// My Window for my own DOM
export class Window implements Window {} // Start implementing lib.dom.d.ts Window.

It would be great if we could use the types from lib.dom.d.ts. We'll see when it can be implemented and how much work it would be.

@valler
Copy link

valler commented Jan 22, 2024

I gave querySelector a try by adding the following to IParentNode.ts:

// We need all the element types. Maybe this should go somewhere else and be imported into here
import type Element from '../element/Element.js';
import type HTMLElement from '../html-element/HTMLElement.js';
import type HTMLAnchorElement from '../html-anchor-element/HTMLAnchorElement.js';
// The list goes on ... 

// The actual map
interface IHTMLElementTagNameMap {
  a: HTMLAnchorElement;
  abbr: HTMLElement;
  address: HTMLElement;
  // ... all of them
}

// Overloaded the signature with two additions
querySelector<K extends keyof IHTMLElementTagNameMap>(
  selectors: K
): IHTMLElementTagNameMap[K] | null;
querySelector<E extends Element = Element>(selectors: string): E | null;
// Keep the existing signature in place
/**
 * Query CSS Selector to find matching node.
 *
 * @param selector CSS selector.
 * @returns Matching element.
 */
querySelector(selector: string): IElement | null;

Added overloads together with the element map and the imports for the corresponding Happy DOM types needed by that map. It's not the only map, and querySelectorAll is still missing as well, but it was already enough to verify that generics and type inference from tag names for that method would work as expected. It's obviously not using lib.dom.d.ts. I am not sure if that would be the direction to go, but from a user's perspective this brings it one step closer together.

I'd be happy to contribute.

@ShivamJoker
Copy link

I am also getting this error:

typescript: Type 'Document' is missing the following properties from type 
'Document': alinkColor, all, anchors, applets, and 55 more.

@capricorn86
Copy link
Owner

@valler return types has been added for querySelector(), querySelectorAll(), createElement(), createElementNS(), getElementsByTagName() and getElementsByTagNameNS():
https://github.com/capricorn86/happy-dom/releases/tag/v13.7.0

However, they return the interfaces and not the actual types. There is a plan to remove the interfaces for Nodes and replace circular dependencies with <T> types instead.

@capricorn86
Copy link
Owner

capricorn86 commented Apr 9, 2024

Happy DOM now returns the correct type for *.querySelector(), *.querySelectorAll(), *.getElementsByTagName(), *.getElementsByTagNameNS(), Document.createElement() and Document.createElementNS():
https://github.com/capricorn86/happy-dom/releases/tag/v13.7.0

Interfaces for DOM related classes has also been removed (e.g. IHTMLButtonElement):
https://github.com/capricorn86/happy-dom/releases/tag/v14.0.0

@ShivamJoker
Copy link

This is great news.
Thanks for doing this @capricorn86.

@capricorn86
Copy link
Owner

This is great news. Thanks for doing this @capricorn86.

Thank you @ShivamJoker!

Just to clarify, this will not make Happy DOM fully compatible with mixing lib.dom.d.ts with Happy DOM types, but it is an improvement with Happy DOM typing in general.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants