Simulating an abstract class with new.target
By Delicious Insights • Published on May 21, 2020

This post is also available in French.

This is nearing the end of our daily series “19 nuggets of vanilla JS.” Today we’ll look at the seldom-known new.target, that showed up with ES2015 and lets us simulate abstract classes, among other things…

The series of 19

Check out surrounding posts from the series:

  1. Converting an object to Map and vice-versa
  2. The for-of loop: should there remain only one…
  3. Simulating an abstract class with new.target (this post)
  4. Negative array indices thanks to proxies

Wait a minute — isn't new an operator?!

Yes it is. Still, JS has a few syntactical oddballs, including new.target, that is legal (inside functions) and references the operand that was passed to new when instantiating the current object.

Consequently, if the current function is not run in the context of an object (if there is no valid resolution of this), it will evaluate to undefined.

Function Environment Record

Every execution of a function spawns a function environment record (FER), that lists all the accessible bindings (associations of values to identifiers) made available by the call to that function. At any time, evaluating a reference traverses a series of active environment records, that roughly align with the relevant function scopes.

The FER for a traditional (non-arrow) function includes four specific bindings dynamically defined at call time: this, arguments, super and new.target. (An arrow function has none of these, its code will therefore resolve these references in the FER of the closest enclosing non-arrow function.)

What is it for?

A common scenario is about abstract classes. Quick reminder: in OOP, an abstract class acts as the starting point of a hierarchy of classes, but is incomplete in itself: you’re not supposed to instantiate it directly.

Let’s illustrate this with the done-do-death hierarchy of geometrical shapes. Sure, they all have a few things in common, like an origin point and methods for drawing and computing the perimeter or area, that are justification enough for a common base class Shape. But a “shape” in itself does not tell us which specific shape to draw, so instantiating new Shape(…) wouldn’t make sense!

JavaScript has no abstract keyword to express this, but we can simulate it by testing, in our constructor, that the operand passed to new wasn’t the base class (i.e. Shape) but rather a subclass (e.g. Square):

class Shape {
  constructor(origin) {
    if (new.target === Shape) {
      throw new Error("Cannot instantiate Shape directly!")
    }
    this.origin = origin
  }

  draw() {
    throw new Error("Must override draw!")
  }
}

class Square extends Shape {
  constructor(origin, size) {
    super(origin)
    this.size = size
  }

  draw() {
    // …
  }
}

new Shape([15, 15])
// => Error: Cannot instantiate Shape directly!

new Square([15, 15], 10).draw()
// => No worries

Where can I get that?!

It’s been natively supported for a while already: Chrome 43, Firefox 41, Opera 33, Edge 13, Safari 11 and Node 5.

Babel and TypeScript transpile as always.

Check out our video course: JavaScript: this is it! 🖥

Get in-depth understanding of how this works in JavaScript, from core ground rules to API overrides to arrow functions, binding and much, much more!