I am totally new to TypeScript, just started learning it 3 days ago. While I was playing around with TypeScript(interfaces), I came across a problem. When I try to do this, it gives me an error:
const divEl:HTMLElement = document.createElement("div");
divEl.someRandomThing = "Some random thing!";
ERROR: error TS2339: Property 'someRandomThing' does not exist on type 'HTMLElement'.
How do I add a custom property(type) to HTMLElement(this interface is from lib.dom.d.ts)?
I tried this:
interface IMyInterface extends HTMLElement {
someRandomThing: string
}
const divEl:IMyInterface = document.createElement("div");
divEl.someRandomThing = "Some random thing!";
It gives me another error:
ERROR: error TS2741: Property 'someRandomThing' is missing in type 'HTMLDivElement' but required in type 'IMyInterface'.
If I add a question mark to "someRandomThing", the error disappear:
interface IMyInterface extends HTMLElement {
someRandomThing?: string //<-----
}
Do I always have to add a question mark? is there a way that I don't need to use the question mark and just add the property(type) into HTMLElement(this interface is from lib.dom.d.ts) interface?
CodePudding user response:
How do I add a custom property(type) to HTMLElement(this interface is from lib.dom.d.ts)?
Your interface is fine for doing that, the problem is that what you get back from createElement
doesn't have your new property, so it doesn't satisfy the requirements of your new interface type, so TypeScript warns you of that since that's TypeScript's primary job. :-)
When you need to extend an object in this way, Object.assign
is a handy way of doing one-offs:
const div = Object.assign(
document.createElement("div"),
{ someRandomThing: "Some random thing!"}
);
The return type of Object.assign
is an intersection of the types of its inputs, in this case HTMLElement & { someRandomThing: string; }
.
For something reusable, you could write a new function that returns IMyInterface
, handling the change in the type of the object internally. That function could either use Object.assign
as above, or just directly assign to the object by internally using a minor type assertion:
function createExtendedElement(tagName: string, someRandomThing: string): IMyInterface {
const element: Partial<IMyInterface> = document.createElement(tagName);
element.someRandomThing = someRandomThing;
return element as IMyInterface;
}
// Usage:
const divEl = createExtendedElement("div", "Some random thing!");
The as IMyInterface
is a type assertion. In general, avoid type assertions, but in well-contained contexts like this kind of utility function, they're okay.
You could generalize that function if you wanted by making it generic and supplying both the property name and value:
function createExtendedElement<Key extends PropertyKey, Value extends any>(
tagName: string,
propName: Key,
value: Value
): HTMLElement & {[P in Key]: Value} {
const element = Object.assign(
document.createElement(tagName),
{[propName]: value}
);
return element as HTMLElement & {[P in Key]: Value};
}
// Usage:
const divEl = createExtendedElement("div", "someRandomThing", "Some random thing!");
Playground link for the above.
One thing you might have noticed looking at the above is that with the very first inline version, the type of the element was HTMLDivElement
(plus someRandomThing
), not jut HTMLElement
(plus someRandomThing
). But the other versions make it just HTMLElement
(plus someRandomThing
).
You can fix that by making the tag name generic as well, lifting the definition of the tag parameter and return type from the ones on createElement
:
function createExtendedElement<
Tag extends keyof HTMLElementTagNameMap,
Key extends PropertyKey,
Value extends any
>(
tagName: Tag,
propName: Key,
value: Value
): HTMLElementTagNameMap[Tag] & {[P in Key]: Value} {
const element = Object.assign(
document.createElement<Tag>(tagName),
{[propName]: value}
);
return element as HTMLElementTagNameMap[Tag] & {[P in Key]: Value};
}
// Usage:
const divEl = createExtendedElement("div", "someRandomThing", "Some random thing!");
In that, divEl
's type is HTMLDivElement & { someRandomThing: string; }
.
Side point: It's largely tangential to the type-related aspects above, but in general, adding your own properties to objects that come from outside your code (in this case, the DOM). (This isn't a TypeScript thing, just a coding thing.) There are a few reasons:
It's really not your object to modify, from an engineering perspective. You can't be sure what that will do to the code that owns the object.
There's potential for conflicts with other code running on the page (including browser extensions). Suppose your code adds a
marker
property to an element, and other code in the page also uses a custommarker
property on elements? Suddenly your code and the other code are cross-talking and may interfere with each other.If done in enough code or a successful enough library, it makes life difficult for (in this case) the WHAT-WG to add new standard properties to DOM elements. Here's a concrete example of that: In ES5, JavaScript's arrays got a new standard method called
some
. It returns a flag for whether any element in the array matches a predicate you supply. So why is it called "some" rather than "any"? Because there was a significant library out in the wild that added its own method calledany
to arrays, and the way the library was written, adding a standard one would have broken code on the websites using that library. So we're stuck withsome
, which doesn't really make sense but was the best available.
In the specific case of the DOM, adding your own property does work reliably on all modern browsers and there's a long history of doing it (jQuery's done it since v1, though with increasingly arcane names). If you choose to do it, make sure you use a name that is very unlikely to conflict with anything now or in the future, or better yet, use a Symbol
as the property key instead, which is guaranteed unique (if you don't make it globally accessible via Symbol.for
).
Whenever you want to add your own information to objects that come from outside your code (and aren't meant to be "owned" by your code), there are at least two things you can do rather than adding your own properties to the objects:
Use composition. For example:
const something = { element: document.createElement("div"), someRandomThing: "Some random thing!" };
Now the element and
someRandomThing
are just held together in the same container. Whenever you needsomeRandomThing
, you usesomething.someRandomThing
. When you need the element, you usesomething.element
.Use a
WeakMap
keyed by the element. For example:// Somewhere you can access it wherever you need it in your app: const elementInfo = new WeakMap(); // ... // Now when you're associating information with the element: const element = document.createElement("div"); elementInfo.set(element, { someRandomThing: "Some random thing!" });
The extra information is in the
WeakMap
keyed by the actual element instance. When you want the element, useelement
. When you want the information you have for it, useelementInfo.get(element)?.someRandomThing
. If the element is removed from the DOM at some point and all other references to it are removed, theWeakMap
doesn't prevent the element from being garbage-collected. Instead, the entry for it in the map disappears.
Just FWIW!