Home > front end >  typescript add new property(type) to existing interface
typescript add new property(type) to existing interface

Time:02-04

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; }.

Playground link


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:

  1. 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.

  2. 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 custom marker property on elements? Suddenly your code and the other code are cross-talking and may interfere with each other.

  3. 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 called any 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 with some, 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:

  1. 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 need someRandomThing, you use something.someRandomThing. When you need the element, you use something.element.

  2. 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, use element. When you want the information you have for it, use elementInfo.get(element)?.someRandomThing. If the element is removed from the DOM at some point and all other references to it are removed, the WeakMap doesn't prevent the element from being garbage-collected. Instead, the entry for it in the map disappears.

Just FWIW!

  • Related