ES2022 feature: class static initialization blocks

[2021-09-01] dev, javascript, es2022
(Ad, please don’t block)

The ECMAScript proposal “Class static initialization blocks” by Ron Buckton is at stage 4 and scheduled to be included in ECMAScript 2022.

For setting up an instance of a class, we have two constructs in JavaScript:

  • Field: Create (and optionally initialize) instance properties.
  • Constructor: A block of code that is executed before setup is finished.

For setting up the static part of a class, we only have static fields. The ECMAScript proposal introduces static initialization blocks for classes, which, roughly, are to static classes what constructors are to instances.

Why do we need static blocks in classes?  

When setting up static fields, using external functions often works well:

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = extractEnglish(this.translations);
  static germanWords = extractGerman(this.translations);
}
function extractEnglish(translations) {
  return Object.keys(translations);
}
function extractGerman(translations) {
  return Object.values(translations);
}

Using the external functions extractEnglish() and extractGerman() works well ins this case because we can see that they are invoked from inside the class and because they are completely independent of the class.

Things become less elegant if we want to set up two static fields at the same time:

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];
  static _ = initializeTranslator( // (A)
    this.translations, this.englishWords, this.germanWords);
}
function initializeTranslator(translations, englishWords, germanWords) {
  for (const [english, german] of Object.entries(translations)) {
    englishWords.push(english);
    germanWords.push(german);
  }
}

This time, there are several issues:

  • Invoking initializeTranslator() is an extra step that either has to be performed outside the class, after creating it. Or it is performed via a workaround (line A).
  • initializeTranslator() does not have access to the private data of Translator.

With a proposed static block (line A), we have a more elegant solution.

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];
  static { // (A)
    for (const [english, german] of Object.entries(this.translations)) {
      this.englishWords.push(english);
      this.germanWords.push(german);
    }
  }
}

A more complicated example  

One way of implementing enums in JavaScript is via a superclass Enum with helper functionality (see the library enumify for a more powerful implementation of this idea):

class Enum {
  static collectStaticFields() {
    // Static methods are not enumerable and thus ignored
    this.enumKeys = Object.keys(this);
  }
}
class ColorEnum extends Enum {
  static red = Symbol('red');
  static green = Symbol('green');
  static blue = Symbol('blue');
  static _ = this.collectStaticFields(); // (A)

  static logColors() {
    for (const enumKey of this.enumKeys) { // (B)
      console.log(enumKey);
    }
  }
}
ColorEnum.logColors();

// Output:
// 'red'
// 'green'
// 'blue'

We need to collect static fields so that we can iterate over the keys of enum entries (line B). This is a final step after creating all static fields. We again use a workaround (line A). A static block would be more elegant.

Details  

The specifics of static blocks are relatively logical (compared to the more complicated rules for instance members):

  • There can be more than one static block per class.
  • The execution of static blocks is interleaved with the execution of static field initializers.
  • The static members of a superclass are executed before the static members of a subclass.

The following code demonstrates those rules:

class SuperClass {
  static superField1 = console.log('superField1');
  static {
    assert.equal(this, SuperClass);
    console.log('static block 1 SuperClass');
  }
  static superField2 = console.log('superField2');
  static {
    console.log('static block 2 SuperClass');
  }
}

class SubClass extends SuperClass {
  static subField1 = console.log('subField1');
  static {
    assert.equal(this, SubClass);
    console.log('static block 1 SubClass');
  }
  static subField2 = console.log('subField2');
  static {
    console.log('static block 2 SubClass');
  }
}

// Output:
// 'superField1'
// 'static block 1 SuperClass'
// 'superField2'
// 'static block 2 SuperClass'
// 'subField1'
// 'static block 1 SubClass'
// 'subField2'
// 'static block 2 SubClass'

Support in engines for class static blocks  

  • V8: unflagged in v9.4.146 (source)
  • SpiderMonkey: behind a flag in v92, intent to ship unflagged in v93 (source)
  • TypeScript: v4.4 (source)

Is JavaScript becoming to much like Java and/or a mess?  

This is a tiny feature that doesn’t compete with other features. We can already run static code via fields with the static _ = ... workaround. Static blocks mean that this workaround isn’t necessary anymore.

Other than that, classes are simply one of many tools in the belt of a JavaScript programmer. Some of us use it, others don’t, and there are many alternatives. Even JavaScript code that uses classes often also uses functions and tends to be lightweight.

Conclusion  

Class static blocks are a relatively simple feature that rounds out the static features of classes. Roughly, it is the static version of an instance constructor. Its mainly useful whenever we have to set up more than one static field.