Skip to main content
A Frehner Site

JavaScript Decorators and Subclassing

Decorators #

It seems the idea for JavaScript decorators has been around for many years. But hey, the current proposal being at Stage 3 doesn't mean nothing, so that's good!

I'm not going to go in-depth into all the ways that these things work together; in fact, I kind of did that several years ago on a different blogging platform, but who knows what kind of wild things were written long ago?!

However, we ran into an issue when subclassing, and I found it interesting enough to share (and save for my future reference, because I have the memory of a goldfish):

Setup #

To set up the issue, let's create a decorator that uses the metadata portion of a decorator to keep track of a list of property names on a class:

function trackPropertyName(_, context) {
	// create a Set if one doesn't exist yet
	if (!context.metadata.properties) {
		context.metadata["properties"] = new Set();
	}

	// add this property to the set
	context.metadata.properties.add(context.name);
}

We now have a Set in the metadata that will keep track of all the properties on our class. Nice!

Next, let's create a class and decorate an auto-accessor (which is VERY nice when working with Decorators! For example, it makes hand-rolling your own web component property-reflection-to-attribute quite easy) with it:

class BaseClass {
	@trackPropertyName
	accessor prop1 = "";
}

And now we have access to the metadata on the class itself, by doing:

BaseClass[Symbol.metadata];

Here's a link to a Babel Repl where you can run it yourself and play with it.

Subclassing - Whoops! #

Let's play around with a subclass of BaseClass now, which has its own unique property that we want to track:

class SubClass extends BaseClass {
	@trackPropertyName
	accessor subProp2 = "";
}

When we run this, the metadata and properties for the subclass look great!

console.log("SubClass properties:", SubClass[Symbol.metadata].properties);
// SubClass properties: Set [ "prop1", "subProp2" ]

Let's check BaseClass's properties, too:

console.log("BaseClass properties:", BaseClass[Symbol.metadata].properties);
// BaseClass properties: Set [ "prop1", "subProp2" ]

Wait, why is "subProp2" in BaseClass's property list? You can see the error for yourself in the browser console in this Babel Repl.

Subclassing - Fix #

Turns out that a subclass' metadata object has its prototype chain set to the parent class' object; that means that when we do

context.metadata.properties.add(context.name);

in the decorator, we're adding it to the parent's Set, up in the prototype chain. Whoops!

Now, this setting-the-prototype-chain stuff is actually desirable behavior, since it would suck to have to recreate all the metadata manually for a subclass instead of getting it automatically. To fix this, we just need our decorator to be a little bit smarter; let's update it like so:

function trackPropertyName(_, context) {
	// check if the metadata object has its own `properties`, or if it's getting it from the prototype
	if (!Object.hasOwn(context.metadata.properties ?? {}, "properties")) {
		// create the new subclass' Set, with data from the parent class (if it exists)
		context.metadata["properties"] = new Set(context.metadata.properties);
	}

	// add this property to the set
	context.metadata.properties.add(context.name);
}

We're now setting each Class' metadata as its own, and copying the data from the parent Class if it's there.

Additionally, Set is perfectly fine with passing undefined to its constructor, so we're safe to always just throw the metadata.properties in there in case it exists (or doesn't exist).

Everyone's happy now!

console.log("SubClass properties:", SubClass[Symbol.metadata].properties);
// SubClass properties: Set [ "prop1", "subProp2" ]

console.log("BaseClass properties:", BaseClass[Symbol.metadata].properties);
// BaseClass properties: Set [ "prop1" ]

Here's the final working Babel Repl.

Fortunately, the Decorator Metadata Proposal outlines this in their README as well, but some of us like to learn through trial-and-error. :)