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