Subclassing arrays in JavaScript
Tags: javascript, node, testing, arrays
Published: 2019-03-31 12:17:27

> Originally posted at [michaelzanggl.com](https://michaelzanggl.com/articles/subclassing-arrays). Subscribe to [my newsletter](https://michaelzanggl.com/) to never miss out on new content.

In my [previous post](https://dev.to/mzanggl/array-methods-and-iterables---stepping-up-your-javascript-game-15bo) I was showing how with various array methods we can reveal our intent. But in the end I was not trully satisfied with the result.

While
```javascript
const usernames = users.map(user => user.name)
```

is definitely much more readable than

```javascript
const usernames = []

users.forEach(user => {
  usernames.push(user.name)
})
```

wouldn't

```javascript
const usernames = users.pluck('name')
```

be even nicer?

So let's see how we can create such functionality. Let's dive into the world of subclassing arrays. We will also look at unit testing in NodeJS as well as a more functional alternative approach.

Btw. I am not promoting some revolutionary new library here. We are simply exploring ideas. I still created a [GitHub repo](https://github.com/MZanggl/subclassing-array-experiments) for this so you can check out the whole code if you want.

---

### But first, how do we create arrays in JavaScript?

The classic

```javascript
const numbers = [1, 2, 3]
```

and the maybe not so well known

```javascript
const numbers = new Array(1, 2, 3)
```

But the above doesn't do what you would expect when you only pass one argument. `new Array(3)` would create an array with three empty values instead of an array with just one value being `3`.

ES6 introduces a static method that fixes that behaviour.

```javascript
const numbers = Array.of(1, 2, 3)
```

Then there is also this

```javascript
const array = Array.from({ length: 3 })
//? (3) [undefined, undefined, undefined]
```
The above works because `Array.from` expects an array-like object. An object with a length property is all we need to create such an object.

It can also have a second parameter to map over the array.
```javascript
const array = Array.from({ length: 3 }, (val, i) => i)
//? (3) [0, 1, 2]
```

---

With that in mind, let's create `Steray`, **Array on Steroids**.

With ES6 and the introduction of classes it is possible to easily extend arrays

```javascript
class Steray extends Array {
    log() {
        console.log(this)
    }
}

const numbers = new Steray(1, 2, 3)
numbers.log() // logs [1, 2, 3]
```

So far so good, but what if we have an existing array and want to turn it into a `Steray`? 

Remember that with `Array.from` we can create a new array by passing an array-like object, and aren't arrays kind of included in that definition?

Which ultimately means we can do this

```javascript
const normalArray = [1, 2, 3]
const steray = Steray.from(normalArray)
```

or alternatively

```javascript
const normalArray = [1, 2, 3]
const steray = Steray.of(...normalArray)
```

---

Let's start adding some methods to `Steray`.
Inside `steray.js` we can just add the long awaited `pluck` method to the class

```javascript
pluck(key) {
    return this.map(item => item[key])
}
```

and that's it. Elegant and powerful.

## Setting up tests

But how do we know this works? We don't know want to go into the browser every time and test our class in the console. So let's quickly set up unit testing, so we can be confident that what we are doing is correct.

Create the following directory structure
```
steray
    src
        steray.js
    test
        sterayTest.js
```

With node and npm installed, install the unit testing framework `mocha` globally.

```bash
npm install mocha -g
```

Next let's initialize `package.json` by running `npm init` in the root of the directory. Follow the instructions until it creates a `package.json` file. When it asks you for the `test` script enter `mocha`. Alternatively you can also change this later inside `package.json`.
```json
"scripts": {
    "test": "mocha"
},
```

Next, install the assertion library `chai` locally

```bash
npm install chai --save-dev
```

And that's all we had to setup. Let's open up `sterayTest.js` and write our first test

```javascript
const expect = require('chai').expect
const Steray = require('../src/steray')

describe('pluck', function() {
    it('should pluck values using the "name" prop', function() {
        const users = new Steray( 
            { name: 'Michael' },
            { name: 'Lukas' },
        )

        const names = users.pluck('name')
        expect(names).to.deep.equal([ 'Michael', 'Lukas' ])
    })
})
```

Run the tests using `npm run test` in the root of the directory and it should output that one test is passing.
With that out of the way we can now safely continue writing new methods, or change the implementation of `pluck` without having to worry about our code breaking.

Let's add some more methods, but this time in the spirit of test driven development!

---

You know what I really don't like? These pesky `for` loops.

```javascript
for (let i; i < 10; i++)
```

Is it `let i` or `const i`, is it `<` or `<=`? Wouldn't it be nice if there was an easier way to achieve this. 
While you can use the syntax we learned earlier `Array.from({ length: 10 }, (value, index) => index)` it is unnecessarily verbose.
Inspired by lodash and Laravel collections, let's create a static `times` method.

In order for you to see the method in action, let's first create the unit test.

```javascript
describe('times', function() {
    it('should return an array containing the indices 0 and 1', function() {
        const numbers = Steray.times(2, i => i)
        expect(numbers).to.deep.equal([ 0, 1 ])
    })
})
```

Try running `npm run test` and it should return errors because `times` doesn't exist yet.

> I will always show the test first, so you can try implementing the method yourself before looking at my implementation. Found a better solution? Send in a PR!

So, here is my implementation of `times` in `steray.js`

```javascript
static times(length, fn) {
    return this.from({ length }, (value, i) => fn(i))
}
```

---

Sometimes you might get confused if there is a long chain and you want to tap into the process to see what is going on. So let's build that functionality.

An example use case would be
```javascript
[1, 2, 3, 4, 5]
    .filter(i => i < 4)
    .map(i => i * 10)
    .tap(console.log)
    .find(i => i === 20)
```

`tap` executes the function but then just returns the very same array again unmodified. `tap` does not return what the callback returns. 
For such a functionality, let's create another method `pipe`.

Here are the tests

```javascript
describe('tapping and piping', function() {
    it('should execute callback one time', function() {
        let i = 0
        new Steray(1, 2, 3).tap(array => i = i + 1)

        expect(i).to.equal(1)
    })

    it('should return original array when tapping', function() {
        const array = new Steray(1, 2, 3).tap(() => 10)
        expect(array).to.deep.equal([1, 2, 3])
    })

    it('should return result of pipe', function() {
        const piped = new Steray(1, 2, 3).pipe(array => array.length)
        expect(piped).to.equal(3)
    })
})
```

And here is the implementation

```javascript
tap(fn) {
    fn(this)
    return this
}
```

```javascript
pipe(fn) {
    return fn(this)
}
```

It's amazing how small yet powerful these methods are! 

---

Remember how in the previous blog post we were turning the `users` array into a hashMap grouped by the `group` key.

Let's also create this functionality by implementing a new method `groupBy`! Here is the test

```javascript
describe('groupBy', function() {
    it('should hashMap', function() {
        const users = new Steray( 
            { name: 'Michael', group: 1 },
            { name: 'Lukas', group: 1 },
            { name: 'Travis', group: 2 },
        )

        const userMap = users.groupBy('group')

        expect(userMap).to.deep.equal({
            '1': [
                { name: 'Michael', group: 1 },
                { name: 'Lukas', group: 1 },
            ],
            '2': [
                { name: 'Travis', group: 2 },
            ]
        })
    })
})
```

and here is the implementation

```javascript
groupBy(groupByProp) {
    return this.reduce((result, item) => {
        const id = item[groupByProp]
        result[id] = result[id] || new []
        
        result[id].push(rest);

        return result;
    }, {})
}
```

While this works, we might run into problems at one point. I will add another unit test to illustrate what can go wrong.

```javascript
it('should hashMap using Steray array', function() {
    const users = new Steray( 
        { name: 'Michael', group: 1 },
        { name: 'Lukas', group: 1 },
        { name: 'Travis', group: 2 },
    )

    const userMap = users.groupBy('group')
    const groupOne = userMap['1']
    const isInstanceOfSteray = (groupOne instanceof Steray)
    expect(isInstanceOfSteray).to.be.true
})
```

What went wrong is `result[id] = result[id] || []`, specifically `[]`. Since we create a normal array, all our newly implemented methods will not be available.

To fix this, let's use `result[id] = result[id] || new Steray` instead.

While the test will pass, the solution is also not 100% clean.
What if we later wanted to move this function into its own file and import it here, wouldn't it create circular dependencies? Also it would be nice if it would be unaware of `Steray`.

A better solution in my opinion is the following 
```javascript
result[id] = result[id] || new this.constructor
```

`this` refers to the steray array and with `this.constructor` we get the class `Steray` dynamically.

---

There is a lot more we can add really
- deduplicating
- chunking
- padding
- prepending data to an array without transforming the original array (unlike `unshift`)

just to name a few. 

You can find the `Steray` class including the unit tests and the above mentioned methods like `chunk`, `pad`, `unique` and `prepend` in the following [GitHub repo](https://github.com/MZanggl/subclassing-array-experiments).

## An alternative to subclassing

Eventually our class may grow into a massive clutter of helper functions and you might run into certain limits.

A different approach would be to go completely functional with [ramda](https://ramdajs.com/).
Ramda has the extra benefit that it also has methods for objects, strings, numbers, even functions.

An example of ramda would be

```javascript
const R = require('ramda')

const users = [
    { name: 'Conan', location: { city: 'Tokyo' } },
    { name: 'Genta', location: { city: 'Tokyo' } },
    { name: 'Ayumi', location: { city: 'Kawasaki' } },
]

const getUniqueCitiesCapitalized = R.pipe(
    R.pluck('location'),
    R.pluck('city'),
    R.map(city => city.toUpperCase()),
    R.uniq()
)
const cities = getUniqueCitiesCapitalized(users)

expect(cities).to.deep.equal(['TOKYO', 'KAWASAKI'])
```

So how about we combine the two, a simple array subclass with the power of consuming ramda functions. I know I know, we are sort of abusing ramda at this point, but it's still interesting to check it out. We just need a new name..., our Array class is not really on steroids anymore, it's quite the opposite, so let' call it `Yaseta`, the Japanese expression when somebody lost weight.

Let's install ramda using `npm install ramda --save-dev` (we only need it for the tests) and create some tests, so we can see how we will use our new library.

```javascript
// test/yasetaTest.js

const expect = require('chai').expect
const Yaseta = require('../src/yaseta')
const pluck = require('ramda/src/pluck')

describe('underscore methods', function() {
    it('returns result of callback', function() {
        const numbers = new Yaseta(1, 2)
        const size = numbers._(array => array.length)

        expect(size).to.equal(2)
    })

    it('returns result of assigned callback using higher order function', function() {
        const users = new Yaseta(
            { name: 'Conan' },
            { name: 'Genta' }
        )

        // this is how ramda works
        const customPluck = key => array => {
            return array.map(item => item[key])
        }

        const usernames = users._(customPluck('name'))

        expect(usernames).to.deep.equal(['Conan', 'Genta'])
    })

    it('can assign ramda methods', function() {
        const users = new Yaseta(
            { name: 'Conan' },
            { name: 'Genta' }
        )

        const usernames = users._(pluck('name'))

        expect(usernames).to.deep.equal(['Conan', 'Genta'])
    })
})
```

And let's create `yaseta.js` in the `src` directory.

```javascript
class Yaseta extends Array {
    _(fn) {
        const result = fn(this)
        return this._transformResult(result)
    }

    _transformResult(result) {
        if (Array.isArray(result)) {
            return this.constructor.from(result)
        }

        return result
    }
}

module.exports = Steray
```

We called the method `_` to take the least amount of space by still providing some readability (at least for people familiar with lodash and such). Well, we are just exploring ideas here anyways.

But what's the deal with `_transformResult`?

See when `ramda` creates new arrays it doesn't do it using `array.constructor`. It just creates a normal array, I guess this is because their `list` functions also work on other iterables. So we would not be able to say

```javascript
numbers
    ._(array => array)
    ._(array => array) // would crash here
```

But thanks to `_transformResult`, we turn it into a `Yaseta` instance again. Let's add another test to see if the above is possible

```javascript
const pluck = require('ramda/src/pluck')
const uniq = require('ramda/src/uniq')
const map = require('ramda/src/map')
// ...
it('can chain methods with ramda', function() {
    const users = new Yaseta(
        { name: 'Conan', location: { city: 'Tokyo' } },
        { name: 'Genta', location: { city: 'Tokyo' } },
        { name: 'Ayumi', location: { city: 'Kanagawa' } },
    )

    const cities = users
        ._(pluck('location'))
        ._(pluck('city'))
        .map(city => city.toUpperCase())
        ._(map(city => city.toUpperCase())) // same as above
        .filter(city => city.startsWith('T'))
        ._(uniq)

        expect(cities).to.deep.equal(['TOKYO'])
})
```

Let's also create a `pipe` method. This time, you can pass as many functions as you need though.

```javascript
describe('pipe', function() {
    it('can pipe methods', function() {
        const users = new Yaseta(
            { name: 'Conan', location: { city: 'Tokyo' } },
            { name: 'Genta', location: { city: 'Tokyo' } },
            { name: 'Ayumi', location: { city: 'Kanagawa' } },
        )

        const cities = users
            .pipe(
                pluck('location'),
                pluck('city'),
                map(city => city.toUpperCase())
            )
            .filter(city => city.startsWith('T'))
            ._(uniq)

            expect(cities).to.deep.equal(['TOKYO'])
    })
})
```

And the implementation in the Yaseta class:

```javascript
pipe(...fns) {
    const result = fns.reduce((result, fn) => {
        return fn(result)
    }, this)

    return this._transformResult(result)
}
```


## Conclusion

So when we compare the different solutions, what do we have now?

**Steray**
```javascript
users = Steray.from(users)
const usernames = users.pluck('name')
```

**Yaseta**
```javascript
users = Yaseta.from(users)
const usernames = users._(pluck('name'))
```

**ramda**
```javascript
const usernames = R.pluck('name')(users)
```

**Vanilla**
```javascript
const usernames = users.map(user => user.name)
```

Each has its own benefits

**Steray**

[+] super readable

[-] subclassing array necessary

[-] manually define methods on class

**Yaseta**

[+] can use all of ramdas methods, but not limited to ramda

[+] OSS contributors could also add more functions that you can install separately.

[-] subclassing array necessary

[-] underscore might throw some off

**ramda**

[+] provides 100% functional approach

[-] We can no longer use dot notation and the `Array.prototype` methods

**Vanilla**

[+] can be used anywhere

[+] no additional learning required for devs

[-] limited to existing methods

---

In most cases the vanilla version is probably good enough, but it's nontheless interesting to see what is possible in JavaScript.

It turns out there is actually another way of handling this kind of thing. Wouldn't it be nice if we could just have dynamic method names on our arrays? Turns out we can!

But that's for next time ;)