Skip to main content
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Cyril Hanquez and Hugo Sombreireiro and Reto Aeberli and Steven Peeters and Guust Nieuwenhuis and Aurélien Deleusière and Damien Bruyndonckx
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Cyril Hanquez Hugo Sombreireiro Reto Aeberli Steven Peeters Guust Nieuwenhuis Aurélien Deleusière Damien Bruyndonckx

TypeScript And .parentNode vs .parentElement

By
Published in Comments (2)

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 the Node interface. However, they state that Internet Explorer only supports it on the Element interface. As such, I'm going to refer to it as Element.parentElement, not Node.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

236 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

15,841 Comments

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel