Here I will go through the π¦ alot π library, which I use to work with arrays in JavaScript/TypeScript. Though there are already dozens of utility libraries for arrays, there was always something I've missed there.
To position the alot
library on a "map" and give you the orientation - I would say, it is somewhere between JavaScript native array methods and Rx
, and in no way completely replaces both of them. Alot
is highly inspired by LINQ
methods from .NET
.
TypeScript and installation
The library is implemented in TypeScript and has complete typings support out of the box, but can be used also in the plain js environment.
$ npm i alot
If you, the reader, still do not use TS on your daily dev basis, I would highly encourage you to switch to typescript. In later articles, I will show how I use TypeScript with no configuration and compilation hassle, and use it as it were a JavaScript (as it actually is, but with sweet additions.)
π± Laziness
Similar to rx
cold observables, the chained methods won't be evaluated until you call subscribe
method - the equivalents in alot
library: toArray
, toArrayAsync
, first
, firstAsync
etc. But let's compare these two examples:
import alot from 'alot'
let arr1 = users
.filter(user => user.score > 10)
.map(user => user.name.toUpperCase())
.slice(0, 5);
let arr2 = alot(users)
.filter(user => user.score > 10)
.map(user => user.name.toUpperCase())
.take(5)
.toArray()
Though the code here does the same things and looks similar, the flow is different. JavaScript methods do the following:
filter
all users by somescore
fieldmap
all previously filtered users to their uppercased names and create a new array of stringstake
(slice) the first5
usernames.
As you may already have noticed, the caveat here is when the users
array has lots of items - the engine goes through all the users to create a new filtered array, then goes over each item to pick the username and create the new array with names.
The example with alot
library has the reversed direction flow. When the js engine evaluates filter
, map
, and take
methods, it builds the query that gets executed when we call the toArray()
method and the flow is:
take
5 elements from the underlyingmap
streammap
users to their uppercased names from the underlyingfilter
stream. As only 5 elements were requested it takes and maps also only 5 elementsfilter
users by somescore
field. As only 5 elements were requested, it processes filtering only until 5 elements are matched
When I'm writing code I still think in forward-flow
filter>map>take
, but understanding how laziness works under the hood is beneficial.
So, in the best case, if the first 5 users have a score greater than 10, then the js engine visits only those 5 elements for filtering and mapping.
I have to mention, that it works great for this example. If we wouldn't have slice/take
methods, then in alot
example we would obviously map
all filtered users and we wouldn't benefit from the laziness nature of alot
lib. That's why I still often use js native array methods, and the alot
API allows me quickly switch between different flows.
The laziness brings us to another feature, which was easy to implement - the asynchronous.
βοΈ Asynchronous
All methods in alot
library have also there asynchronous variations, which accept async methods.
From the previous example, what if we would want to load also user's comments along with uppercased username - for native js methods we have a huge headache, how to accomplish this. I have seen such wrong β solution:
let promises = users
.filter(user => user.score > 10)
.map(async user => {
return {
username: user.name.toUpperCase(),
comments: await Api.loadComments(user.id)
};
})
.slice(0, 5);
let arr1 = await Promise.all(promises);
Wrong things about this could be:
- here we load comments for all filtered users, even though we need only 5.
- even, if we would need all users, not only 5, we make anyway all requests parallel - which means if we have an array with
1000
elements - we make1000
requests simultaneously. (Though browsers and nodejs have max number for open connections, not all requests go immediately to the server, but it could cause timeout errors anyway)
With alot
library it would be almost similar and β
let arr2 = await alot(users)
.filter(user => user.score > 10)
.mapAsync(async user => {
return {
username: user.name.toUpperCase(),
comments: await Api.loadComments(user.id)
};
})
.takeAsync(5)
.toArrayAsync({ threads: 2})
And the previous issues are solved:
- due to the
take
method, wemap
only 5 elements, which means we make only 5 api requests to load the comments. alot
library can handle the async queue - in this example, we make2
requests to the server at once (this number is just an example, you can set any number of simultaneous async tasks, default is4
)
So with little code change, we managed to create a lazy asynchronous stream
We can also make our filter
async, in case we have to get the data for filtering also async.
let arr2 = await alot(users)
.filterAsync(user => Api.isActive(user))
.mapAsync(async user => {
return {
username: user.name.toUpperCase(),
comments: await Api.loadComments(user.id)
};
})
.takeAsync(5)
.toArrayAsync({ threads: 2 })
Πttentive reader could notice, that such data loading is not optimal. Batch load by providing the array of userIds to the backend would be better. This example just demonstrates the async tasks. This is just a tool, how and when to use it depends on you.
βοΈ Additional array methods.
There are some other convenient methods to work with arrays. Check the Documentation
The methods I use often are:
groupBy(user => user.score)
Returns stream of groups ({ key, values: T[] }[]
) grouped by the same value, by the score in this example.distinctBy(user => user.city)
Returns stream of unique items by the field value. In this example: a user per city.sortBy(user => user.age, 'asc')
Returns sorted stream. Possible directionsasc
anddesc
. For better text sorting there issortByLocalCompare
method.toMap(user => user.id, user => user.name)
Returns anMap
. From the example, the keys would be the ID of a user, and the value would be the name.I use this also to improve performance. Imagine, you need to query the name of a user by id often. Then instead of
users.find(x =>x.id === id)?.name
I create aMap
ofid:name
and then simply get the name:map.get(id)
toDictionary
method returnsobject
instead ofMap
, just in case it is more convinient for you
Happy coding
π
Did you find this article valuable?
Support Alex Kit by becoming a sponsor. Any amount is appreciated!