Since ES6, JavaScript enjoys support for classes and static functions akin to static functions in other object-oriented languages. Unfortunately, JavaScript lacks support for static properties, and recommended solutions on Google fail to take into account inheritance. I ran into this problem when implementing a new Mongoose feature that requires a more robust notion of static properties. Specifically, I need static properties that support inheritance via setting prototype or via extends . In this article, I'll describe a pattern for implementing static properties in ES6.

Static Methods and Inheritance

Suppose you have a simple ES6 class with a static method.

class Base { static foo() { return 42 ; } }

You can use extends to create a subclass and still have access to the foo() function.

class Sub extends Base {} Sub.foo();

You can also use static getters and setters to set a static property on the Base class.

let foo = 42 ; class Base { static get foo() { return foo; } static set foo(v) { foo = v; } }

Unfortunately, this pattern has undesirable behavior when you subclass Base . If you set foo on a subclass, it will set foo for the Base class and all other subclasses.

class Sub extends Base {} console .log(Base.foo, Sub.foo); Sub.foo = 43 ; console .log(Base.foo, Sub.foo);

The problem gets worse if your property is an array or an object. Because of prototypical inheritance, if foo is an array, every subclass will have a reference to the same copy of the array as shown below.

class Base { static get foo() { return this ._foo; } static set foo(v) { this ._foo = v; } } Base.foo = []; class Sub extends Base {} console .log(Base.foo, Sub.foo); Sub.foo.push( 'foo' ); console .log(Base.foo, Sub.foo); console .log(Base.foo === Sub.foo);

So JavaScript supports static getters and setters, but using them with objects or arrays is a footgun. Turns out you can do it with a little help from JavaScript's built-in hasOwnProperty() function.

Static Properties With Inheritance

The key idea is that a JavaScript class is just another object, so you can distinguish between own properties and inherited properties.

class Base { static get foo() { return this .hasOwnProperty( '_foo' ) ? this ._foo : void 0 ; } static set foo(v) { this ._foo = v; } } Base.foo = []; class Sub extends Base {} console .log(Base.foo, Sub.foo); console .log(Base.foo === Sub.foo); Base.foo.push( 'foo' ); console .log(Base.foo, Sub.foo); console .log(Base.foo === Sub.foo);

This pattern is neat with classes, but it also works with pre-ES6 JavaScript inheritance. This is important because Mongoose still uses pre-ES6 style inheritance. In hindsight we should have switched sooner, but this feature is the first time we've seen a clear advantage to using ES6 classes and inheritance over just setting a function's prototype .

function Base ( ) {} Object .defineProperty(Base, 'foo' , { get: function ( ) { return this .hasOwnProperty( '_foo' ) ? this ._foo : void 0 ; }, set: function ( v ) { this ._foo = v; } }); Base.foo = []; function Sub1 ( ) {} Sub1.prototype = Object .create(Base.prototype); Object .defineProperty(Sub1, 'foo' , Object .getOwnPropertyDescriptor(Base, 'foo' )); class Sub2 extends Base {} console .log(Base.foo, Sub1.foo); console .log(Base.foo, Sub2.foo); Base.foo.push( 'foo' ); console .log(Base.foo, Sub1.foo); console .log(Base.foo, Sub2.foo);

Moving On