Is there a way to both add a property and extend an interface or type simultaneously, without resorting to using ts-ignore or casting with "as"?

In my quest to enhance an HTMLElement, I am aiming to introduce a new property to it.

type HTMLElementWeighted = HTMLElement & {weight : number}

function convertElementToWeighted(element : HTMLElement, weight : number) : HTMLElementWeighted {
  element.weight = weight // <-- TypeScript throws errors at me here as weight is not part of element
  return element
}

I realize that using @ts-ignore or as can resolve this issue, but I prefer finding a more elegant solution. It feels like a hack otherwise.

Answer №1

When creating a new property for an object, it is possible to do so without changing the original object by spreading it into a new one.

// AVOID THIS
function convertElementToWeighted(element : HTMLElement, weight : number) : HTMLElementWeighted {
    return { ...element, weight };
}

While this method works well for plain objects and satisfies TypeScript requirements, it may not be ideal for HTMLElements. This is because the new object no longer references the actual element in the DOM, which could cause issues with other parts of the script that rely on the original reference. Additionally, only enumerable own properties are extracted from the element, potentially resulting in a JavaScript object with no properties at all. TypeScript does not differentiate between enumerable vs non-enumerable or own vs inherited properties.

Therefore,

  • Modifying objects to add non-existing properties is generally not recommended (while possible in TypeScript, it would alter the type of all HTMLElements)
  • Attempting to clone the element to add the new property is also not advisable, as the cloned element will not reference the original one

It is best to leave the element unchanged and create a new data structure that includes the element and any additional data needed. For example:

function convertElementToWeighted(element: HTMLElement, weight: number) {
  return {
    element,
    weight,
  };
}

To maintain the specific type of the passed element, use generics for increased flexibility. For instance, if you pass in an HTMLAnchorElement, it's preferable for the returned type to also be HTMLAnchorElement instead of a generic HTMLElement.

function convertElementToWeighted<T extends HTMLElement>(element: T, weight: number) {

Answer №2

If you have a function that needs to modify its argument by adding a property and then return the updated object, you can achieve this easily using the Object.assign() method. This approach bypasses the need for a type assertion, providing the desired intersection result:

function addWeightToElement(
  element: HTMLElement, weight: number
): HTMLElementWeighted {
  return Object.assign(element, { weight }); // works fine
}

To make it even more versatile, we can make the function generic so that it can be applied to any subtype of HTMLElement:

type HTMLElementWeighted<T extends HTMLElement> = T & { weight: number }

function addWeightToElement<T extends HTMLElement>(
  element: T, weight: number
): HTMLElementWeighted<T> {
  return Object.assign(element, { weight });
}

Now let's run some tests:

const img = document.createElement("img");
img.weight; // error, weight property not found on HTMLImageElement
const weightedImg = addWeightToElement(img, Math.PI);
weightedImg; // const weightedImg: HTMLElementWeighted<HTMLImageElement>
console.log(weightedImg.weight.toFixed(2)) // "3.14"
console.log(weightedImg.width); // 0

The compiler recognizes weightedImg as

HTMLElementWeighted<HTMLImageElement>
, allowing access to both the added weight property and the original HTMLImageElement-specific properties.

One drawback is that after calling addWeightToElement(), you essentially lose the reference to your initial img object and must use the returned weightedImg if you want to access the weight property. While they refer to the same object at runtime, the compiler does not recognize the type change on img. It's something to keep in mind.


Alternatively, if you prefer not to use the return value and instead narrow the apparent type of the function argument, you can turn addWeightToElement() into an assertion function like this:

function addWeightToElement<T extends HTMLElement>(element: T, weight: number
): asserts element is HTMLElementWeighted<T> {
  Object.assign(element, { weight });
}

In this case, the function doesn't return anything but rather declares a return type of the assertion predicate

asserts element is HTMLElementWeighted<T>
. Let's test it out:

const img = document.createElement("img");
img.weight; // error, weight property not found on HTMLImageElement
convertElementToWeighted(img, Math.PI);
img; // const img: HTMLElementWeighted<HTMLImageElement>
console.log(img.weight.toFixed(2)) // "3.14"
console.log(img.width); // 0

Prior to calling addWeightToElement, the compiler rejects img.weight, but afterward, it recognizes img as

HTMLElementWeighted<HTMLImageElement>
, enabling access to the weight property while preserving the HTMLImageElement functionality. The same object reference, img, is sustained throughout.

Keep in mind that assertion functions cannot return anything, so you need to decide whether to utilize the function argument or its post-call state; you can't do both.

Playground link to code

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The function database is not defined in firebase_compat_app__WEBPACK_IMPORTED_MODULE_0__.default

Encountering an error message when attempting to connect my app to Firebase: firebase_compat_app__WEBPACK_IMPORTED_MODULE_0__.default.database is not a function After some testing, it seems the issue only arises when trying to connect to the database. The ...

Issue with closing the active modal in Angular TypeScript

Can you help me fix the issue with closing the modal window? I have written a function, but the button does not respond when clicked. Close Button: <button type="submit" (click)="activeModal.close()" ...

dynamic padding style based on number of elements in array

Is there a way to set a padding-top of 10px only if the length of model.leaseTransactionDto.wagLeaseLandlordDto is greater than 1? Can someone provide the correct syntax for conditionally setting padding based on the length? Thank you. #sample code <d ...

"Exploring the differences between normalization structures and observable entities in ngrx

I'm currently grappling with the concept of "entity arrays" in my ngrx Store. Let's say I have a collection of PlanDTO retrieved from my api server. Based on the research I've done, it seems necessary to set up a kind of "table" to store th ...

Typing a general d3 selection to target various types of SVG elements

I am looking for a callback function that can be utilized with both rect and circle elements. Here is an example: function commonTasks(selection:d3.Selection<PLACEHOLDER_TYPE, MyDataType, SVGGElement, unknown>) { selection .classed('my-c ...

Having trouble locating the namespace for angular in typescript version 1.5

I am using Angular 1.5 with TypeScript and have all the necessary configurations in my tsconfig.json file. However, when I run tslint, I encounter numerous errors in the project, one of which is: Cannot find namespace angular My tsconfig.json file looks ...

When the query result is received in Angular TypeScript, translate epoch time into a time string

Here is the dilemma I am currently facing: I have an Angular script that requests data from a backend service and receives query results to display to the user. One of the fields in the query response is a time stamp, which is currently in epoch time forma ...

Creating both Uniform and Varying drawings on a single webGL canvas

My goal is to create this specific illustration. https://i.sstatic.net/5AfdW.png This project requires the usage of TypeScript. The Code: The code is organized across multiple files. Within the scenegraph file, there's a function that visits a gro ...

Enhance the variety of types for an external module in TypeScript

I am in the process of migrating an existing codebase from a JavaScript/React/JSX setup to TypeScript. My plan is to tackle this task file by file, but I have a question regarding the best approach to make the TypeScript compiler work seamlessly with the e ...

Unable to adjust the text color to white

As someone who is new to Typescript, I'm facing a challenge in changing the text color to white and struggling to find a solution. I'm hoping that someone can guide me in the right direction as I've tried multiple approaches without success ...

In React-Native, implement a function that updates one state based on changes in another state

I need to trigger a function when a specific state changes. However, I encountered the error 'maximum update depth reached'. This seems illogical as the function should only respond to changes from stateA to update stateB. I attempted using setSt ...

Pause and anticipate the subscription within the corresponding function

Is there a way to make an If-Else branch wait for all REST calls to finish, even if the Else side has no REST calls? Let's take a look at this scenario: createNewList(oldList: any[]) { const newList = []; oldList.forEach(element => { if (eleme ...

I am looking to extract only the alphanumeric string that represents the Id from a MongoDB query

Working with mongoDB, mongoose, and typescript, I am facing an issue where I need to preserve the document ids when querying. However, all I can retrieve is the type _id: new ObjectId("62aa4bddae588fb13e8df552"). What I really require is just the string ...

The test session failed to launch due to an error in initializing the "@wdio/cucumber-framework" module. Error message: [ERR_PACKAGE_PATH_NOT_EXPORTED]

I added @wdio/cli to my project using the command 'npm i --save-dev @wdio\cli'. Next, I ran 'npx wdio init' and chose 'cucumber', 'selenium-standalone-service', 'typescript', 'allure' along w ...

Troubles with Katex/ngx-markdown Display in Angular 16

In my Angular 16 application, I utilize the ngx-markdown library alongside Katex and other dependencies. A challenging situation arises when the backend (an LLM) responds with markdown text that conflicts with Katex delimiters during rendering. I attempte ...

Developing a firestore query using typescript on a conditional basis

I've encountered an issue while attempting to create a Firestore query conditionally. It seems like there's a TypeScript error popping up, but I can't seem to figure out what's causing it. Here's the snippet of my code: const fetch ...

Enhancing Angular2 authentication with Auth0 for enabling Cross-Origin Resource Sharing

I have been working on implementing user authentication through Auth0. I followed the instructions provided on their website, but I am encountering authentication issues. Whenever I try to authenticate, an error message appears in the console stating that ...

Vue composable yields a string value

I am currently using a Vue composable method that looks like this: import { ref } from 'vue'; const useCalculator = (num1: number, num2: number, operation: string) => { const result = ref(0); switch (operation) { case 'add& ...

Exploring the Features of PrimeNG Table Component in Angular 8

After attempting to implement p-table (PrimeNG table) in my Angular project and importing all necessary dependencies and modules using the CLI, I encountered the following error: ERROR: The target entry-point "primeng/table" has missing dependencies: - @ ...

I am having trouble reaching the _groups attribute in angular/d3js

I am encountering an issue when trying to access the "_groups" property in my code: function getMouseDate(scale){ var groupElement = d3.select("#group")._groups[0][0] var xCoordinate = scale.invert(d3.mouse(groupElement)[0]); co ...