JS Tip of the Day: Symbol.species

Symbol.species
Version: ES2015
Level: Advanced

The species of an object is used to determine the kind of objects it creates when creating derived versions of itself. The slice() method of arrays, for example, creates and returns a new array when its called. If you were to extend Array to create your own custom array class, slice() called from instances of that class would create new instances of the custom array class rather than normal arrays.

console.log(new Array(1, 2, 3).slice(1) instanceof Array); // true

class CustomArray extends Array {}
let sliced = new CustomArray(1, 2, 3).slice(1);
console.log(sliced instanceof CustomArray); // true

By default the species will use the current class to create derived instances, but you can specify a new type by creating a static Symbol.species property on the class. The value of this property will represent the constructor to use for derived instances.

class CustomArray extends Array {
    static get [Symbol.species] () {
        return Array; // normal arrays now the species
    }
}

let sliced = new CustomArray(1, 2, 3).slice(1);
console.log(sliced instanceof CustomArray); // false
console.log(sliced instanceof Array); // true

Constructors used from species should have the same signature as those used by the base type. In the case of Array species, this means a single numeric length argument or any number of other arguments representing the elements of the array. If you have an Array subclass that has special constructor requirements, you may want to use Array for its species (as seen above) so methods like slice() won’t fail when they attempt to create new instances for their return values.

Symbol.species is supported by Array (including typed arrays and array buffers), Promise, and RegExp. Map and Set also define a Symbol.species property, but there’s nothing in place within their current API that uses it. The expectation is that if you (or anyone else) extended these types, the species would be used for new instances if they were needed.

class SlicableSet extends Set {
    slice (begin, end) {
        let Species = this.constructor[Symbol.species];
        return new Species(
          Array.from(this)
            .slice(begin, end)
        );
    }
}

let pizza = new SlicableSet([1,2,3,4,5,6]);
let plate = pizza.slice(1,3);
console.log(plate instanceof SlicableSet); // true
console.log(plate); // SlicableSet {2, 3}

Species can be also important for promises. This is because promises create new promises via species for every call to then(), catch(), and finally(). As part of a promise chain, you may not want the behavior of your custom promise to be propagated through the rest of the entire chain. We’ll explore this specific case more in a future tip.

More info: