Upgrading JavaScript's Collections

Phil Nash
Developer relations engineer at DataStax

Phil Nash

twitter.com/philnash

linkedin.com/in/philnash

philna.sh/links

Phil Nash DataStax logo

Upgrading JavaScript's Collections

ES2015 >>> ES2024

From Promises...

function similaritySearch(content) {
	return collection.find({}, { sort: { $vectorize: content }, limit: 5 })
	  .then(cursor => cursor.toArray())
		.then(results => results.join("\n\n"))
		.catch(error => console.error);
}

...to async/await...

async function similaritySearch(content) {
  try {
		const cursor = await collection.find({}, { sort: { $vectorize: content }, limit: 5 });
		const results = await cursor.toArray();
		return results.join("\n\n");
	} catch (error) {
		console.log(error);
	}
}

...to Symbols as WeakMap keys

// Step 1
const weak = new WeakMap();
const key = Symbol('my ref');
const someObject = { foo: 'bar' };
weak.set(key, someObject);

// Step 2
// ???

// Step 3
// Profit!

Keeping up with JavaScript

Set

What do you use Set for?

Making unique arrays

const uniqueArray = Array.from(new Set(array));

Fast membership checks

array.includes?(item);
// O(n)
set.has(item);
// O(1)

Missing set operations

const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages  = new Set(["Python", "Java", "JavaScript"]);

const allLanguages = new Set([...frontEndLanguages, ...backEndLanguages]);

// => Set(5) {"JavaScript", "HTML", "CSS", "Python", "Java"}

Missing set operations: union

const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages  = new Set(["Python", "Java", "JavaScript"]);

const allLanguages = frontEndLanguages.union(backEndLanguages);

// => Set(5) {"JavaScript", "HTML", "CSS", "Python", "Java"}

Missing set operations

const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages  = new Set(["Python", "Java", "JavaScript"]);

const frontAndBackLanguages = new Set([...frontEndLanguages].filter(item => {
  return backEndLanguages.has(item);
}));

// => Set(1) {"JavaScript"}

Missing set operations: intersection

const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages  = new Set(["Python", "Java", "JavaScript"]);

const frontAndBackLanguages = frontEndLanguages.intersection(backEndLanguages);

// => Set(1) {"JavaScript"}

Missing set operations

const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages  = new Set(["Python", "Java", "JavaScript"]);

const onlyFrontEnd = new Set([...frontEndLanguages].filter(item => {
  return !backEndLanguages.has(item);
}));

// => Set(1) {"HTML", "CSS"}

Missing set operations: difference

const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages  = new Set(["Python", "Java", "JavaScript"]);

const frontAndBackLanguages = frontEndLanguages.difference(backEndLanguages);

// => Set(1) {"HTML", "CSS"}

Missing set operations

const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages  = new Set(["Python", "Java", "JavaScript"]);

const onlyFrontEnd = [...frontEndLanguages].filter(item => 
  !backEndLanguages.has(item));
const onlyBackEnd = [...backEndLanguages].filter(item => 
  !frontEndLanguages.has(item));

const singleEnvLanguages = new Set([...onlyFrontEnd, ...onlyBackEnd]);

// => Set(1) {"HTML", "CSS", "Python", "Java"}

Missing set operations:
symmetric difference

const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages  = new Set(["Python", "Java", "JavaScript"]);

const singleEnvLanguages = frontEndLanguages.symmetricDifference(backEndLanguages);

// => Set(1) {"HTML", "CSS", "Python", "Java"}

Missing set operations: comparison

const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages  = new Set(["Python", "Java", "JavaScript"]);
const markupAndStyle  = new Set(["HTML", "CSS"]);

markupAndStyle.isSubsetOf(frontEndLanguages);
// => true

frontEndLanguages.isSupersetOf(markupAndStyle);
// => true

markupAndStyle.isDisjointFrom(backEndLanguages);
// => true

Why so long?

Set operations support: ES2025

Chrome
122
Firefox
127
Edge
122
Safari
17
Opera
108

Throw away your lodash

_.union(array1, array2);

lodash.union receives 6.5 million downloads/week

_.difference(array1, array2);

lodash.difference receives 6.5 million downloads/week

Array

🔥 Immutability 🔥

Confusion

const array = ["JavaScript", "HTML", "CSS"];
const sorted = array.sort();

console.log(sorted);
// => ["CSS", "HTML", "JavaScript"]

console.log(array);
// => ["CSS", "HTML", "JavaScript"]

Unnecessary

const array = ["JavaScript", "HTML", "CSS"];
const sorted = [...array].sort();

console.log(sorted);
// => ["CSS", "HTML", "JavaScript"]

console.log(array);
// => ["JavaScript", "HTML", "CSS"]

Array copying methods: toSorted

const array = ["JavaScript", "HTML", "CSS"];
const sorted = array.toSorted();

console.log(sorted);
// => ["CSS", "HTML", "JavaScript"]

console.log(array);
// => ["JavaScript", "HTML", "CSS"]

Array copying methods: toReversed

const array = ["JavaScript", "HTML", "CSS"];
const reversed = array.toReversed();

console.log(reversed);
// => ["CSS", "HTML", "JavaScript"]

console.log(array);
// => ["JavaScript", "HTML", "CSS"]

Array copying methods: toSpliced

const array = ["JavaScript", "HTML", "CSS"];
const updated = array.toSpliced(0, 1, "TypeScript");

console.log(updated);
// => ["TypeScript", "HTML", "CSS"]

console.log(array);
// => ["JavaScript", "HTML", "CSS"]

Splice usages

const array = ["JavaScript", "HTML", "CSS"];
const removed = array.splice(0, 1, "TypeScript");

console.log(removed);
// => ["JavaScript"]

console.log(array);
// => ["TypeScript", "HTML", "CSS"]

Copy with slice

const array = ["JavaScript", "HTML", "CSS"];
const removed = array.slice(0, 1);

console.log(removed);
// => ["JavaScript"]

console.log(array);
// => ["JavaScript", "HTML", "CSS"]

Array copying methods: with

const array = ["JavaScript", "HTML", "CSS"];
array[0] = "TypeScript";

console.log(array);
// => ["TypeScript", "HTML", "CSS"]

Array copying methods: with

const array = ["JavaScript", "HTML", "CSS"];
const updated = array.with(0, "TypeScript");

console.log(updated);
// => ["TypeScript", "HTML", "CSS"]

console.log(array);
// => ["JavaScript", "HTML", "CSS"]

Array copying methods: caveats

class MyArray extends Array {};
const languages = new MyArray("JavaScript", "TypeScript", "CoffeeScript");

const upcasedLanguages = languages.map(language => language.toUpperCase());
console.log(upcasedLanguages instanceof MyArray);
// => true

const reversed = languages.toReversed();
console.log(reversed instanceof MyArray);
// => false
            

Array copying methods: caveats

class MyArray extends Array {};
const languages = new MyArray("JavaScript", "TypeScript", "CoffeeScript");

const reversed = MyArray.from(languages.toReversed());
console.log(reversed instanceof MyArray);
// => true
            

Array copying support: ES2023

Chrome
110
Firefox
115
Edge
110
Safari
16
Opera
96

Object and Map

Grouping into an Object

const people = [
  { name: "Alice", age: 28 }, { name: "Bob", age: 30 }, { name: "Eve", age: 28 }
];

const peopleByAge = {};

people.forEach((person) => {
  const age = person.age;
  if (!peopleByAge[age]) {
    peopleByAge[age] = [];
  }
  peopleByAge[age].push(person);
});
console.log(peopleByAge);
/*
{
  "28": [{"name":"Alice","age":28}, {"name":"Eve","age":28}],
  "30": [{"name":"Bob","age":30}]
}
*/

Grouping into an Object: groupBy

const people = [
  { name: "Alice", age: 28 }, { name: "Bob", age: 30 }, { name: "Eve", age: 28 }
];

const peopleByAge = Object.groupBy(people, (person) => person.age);

console.log(peopleByAge);
/*
{
  "28": [{"name":"Alice","age":28}, {"name":"Eve","age":28}],
  "30": [{"name":"Bob","age":30}]
}
*/

Grouping into an Object:
null-prototype object

const people = [
  { name: "Alice", age: 28 }, { name: "Bob", age: 30 }, { name: "Eve", age: 28 }
];

const peopleByAge = Object.groupBy(people, (person) => person.age);

console.log(peopleByAge.hasOwnProperty("28"));
// TypeError: peopleByAge.hasOwnProperty is not a function

Grouping into an Map: groupBy

const ceo = { name: "Jamie", age: 40, reportsTo: null };
const manager = { name: "Alice", age: 28, reportsTo: ceo };

const people = [
  ceo,
  manager,
  { name: "Bob", age: 30, reportsTo: manager },
  { name: "Eve", age: 28, reportsTo: ceo },
];

const peopleByManager = Map.groupBy(people, (person) => person.reportsTo);

console.log(peopleByManager.get(ceo));
/* => [
  { name: "Alice", age: 28, reportsTo: ceo },
  { name: "Eve", age: 28, reportsTo: ceo }
] */

Grouping into an Map: groupBy

peopleByManager.get({ name: "Jamie", age: 40, reportsTo: null });
// => undefined

Naming shenanigans

Or why is it Object.groupBy and not Array.prototype.group?

Array grouping methods: ES2024

Chrome
117
Firefox
119
Edge
117
Safari
17.4
Opera
103

Throw away your lodash

_.groupBy(people, person => person.age);

lodash.groupby receives 2.5 million downloads/week

The future
🔮

🔥 Immutability 🔥

Records and Tuples

Records and Tuples

Built-in immutable objects

Compound primitives

Deep immutability

New syntax

Records and Tuples

const tuple = #[1,2,3];
const record = #{ name: "Alice", age: 28 };
const newTuple = tuple.with(0, 4);
// => #[4,2,3]
record === #{ age: 28, name: "Alice" };
// => true
record["name"] = "Betty";
// TypeError '"name" is read-only'
const recordWithObject = #{ name: "Betty", dob: new Date(1984, 8, 14) };
// TypeError 'cannot use an object as a value in a record'

Symbols as WeakMap keys

const registry = new WeakMap();
const dob = Symbol('dob');
registry.set(dob, new Date(1984, 8, 14));

const record = #{ name: "Betty", dob: dob };

console.log(registry.get(record.dob));
// => "Fri Sep 14 1984"

Records and Tuples support: TC39 stage 2

Chrome
Firefox
Edge
Safari
Opera

Records and Tuples

Check out the playground:

https://rickbutton.github.io/record-tuple-playground/

JavaScript keeps improving

Links

Thanks

Phil Nash DataStax logo