Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Luis Majano
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Luis Majano@lmajano )

TypeScript And .parentNode vs .parentElement

By Ben Nadel on

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.



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

Reply to this Comment

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

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.