TypeScript And .parentNode vs .parentElement
For years (?decades?), I've been using .parentNode
to travel up the DOM (Document Object Model) tree. And, to be honest, I thought that was the only traversal option we had. However, over the weekend as I was perusing the Mozilla Developer Network (MDN) documentation - as you do - I happened to notice the property Element.parentElement
. The .parentElement
property is similar to the .parentNode
property; but, if you're coding in TypeScript, the difference between the two is very exciting!
NOTE: I am exploring this in the context of Angular; however, this is not specific to Angular - it will be relevant for any web application that uses the DOM and TypeScript.
Like Node.parentNode
, the Element.parentElement
property points to the parent Element
in the DOM tree. Which - in the vast majority of cases - is exactly the same as .parentNode
. At least, pragmatically. However, when we are dealing with TypeScript, pragmatic and semantic are often at odds with each other. This is why we have to use TypeScript constructs like type-casting, the Definite Assignment Assertion, and the Non-Null assertion: in cases where TypeScript cannot deduce the runtime state, we have to step-in and guide the compiler.
ASIDE: According to MDN, the
.parentElement
is a property of theNode
interface. However, they state that Internet Explorer only supports it on theElement
interface. As such, I'm going to refer to it asElement.parentElement
, notNode.parentElement
.
Take, for example, handling a click
event in Angular and then trying to walk up the DOM tree to find a parent element with a given class (.bar
):
@Component({
selector: "app-root",
styleUrls: [ "./app.component.less" ],
template:
`
<div class="foo">
<div class="bar">
<div class="baz">
<p>
<a (click)="handleClick( $event.target )">Find .bar</a>
</p>
</div>
</div>
</div>
`
})
export class AppComponent {
public handleClick( target: HTMLElement ) : void {
var barElement: HTMLElement | null = target;
// Continue walking up the DOM Tree until we find ".bar".
while ( barElement && ! barElement.classList.contains( "bar" ) ) {
barElement = barElement.parentNode;
}
console.log( "FOUND .bar !!" );
console.log( barElement );
}
}
If we try to compile this, TypeScript will throw the following error:
Type '(Node & ParentNode) | null' is not assignable to type 'HTMLElement | null'. Type 'Node & ParentNode' is not assignable to type 'HTMLElement | null'.
The problem here is that .parentNode
doesn't return an Element
, it returns a Node
. So, we could try changing the barElement
declaration to use Node
:
var barElement: Node | null = target;
But, all that does is change the TypeScript error:
Property 'classList' does not exist on type 'Node'.
Again, as humans, we know that .parentNode
, in this case, is going to return an Element
. So, we might try to cast the value:
public handleClick( target: HTMLElement ) : void {
var barElement: HTMLElement | null = target;
while ( barElement && ! barElement.classList.contains( "bar" ) ) {
barElement = ( barElement.parentNode as HTMLElement );
}
console.log( "FOUND .bar !!" );
console.log( barElement );
}
Here, we're down-casting Node
to HTMLElement
during the traversal - essentially telling TypeScript that we know what's really going on at runtime and that it should ignore its compile-time instincts.
And, that's why I am so excited to have discovered .parentElement
. Now, I can just do this:
public handleClick( target: HTMLElement ) : void {
var barElement: HTMLElement | null = target;
while ( barElement && ! barElement.classList.contains( "bar" ) ) {
barElement = barElement.parentElement;
}
console.log( "FOUND .bar !!" );
console.log( barElement );
}
This compiles perfectly well - no casting, no assertions, no nothing. Just clear code demonstrating proper semantics and run-time intentions.
One of the most powerful benefits of moving from JavaScript to TypeScript is that you are forced to codify all of your intentions with Types. You can't rely on things being "coincidentally true" at run-time. Instead, you have to stop and really think about all of your assumptions. By switching from .parentNdoe
to .parentElement
(in cases where it makes sense), I can stop using "pragmatically true" facts and start using "semantically true" facts. And, that's pretty exciting to me!
Epilogue on Run-Time Truths
I am not intending to imply that you should never have to tell TypeScript what is actually happening at run-time. The reality is, you have to, at least some of the time. I am only trying to minimize the degree to which I have to do that. And, the more I can push facts down into the TypeScript compiler, the safer the code becomes.
Want to use code from this post? Check out the license.
Reader Comments
Thanks! Any idea when the Node interface would be the better interface to choose? If it doesn't have classList. And when would Node.parentNode != Element.parentElement?
Also, you have a minor typo...parentNdoe
@Chris,
Hmmm, I am not sure. At the very root of the document, the two are different:
document.body.parentElement.parentElemnt
=>null
document.body.parentElement.parentNode
=>document
But, for the most part, if you're working with user-interactions inside the app, no one is going up that far? Not sure. I guess it depends. But, I'll stick to
parentElement
for now, until it breaks.