Find a matching element in a list and move to first: A journey from a novice JavaScript to an elegant TypeScript solution
Here’s the problem we’re trying to solve. We have an array of users as follow:
const users = [
{ firstName: 'Jane', lastName: 'Foo' },
{ firstName: 'John', lastName: 'Bar' },
{ firstName: 'Jill', lastName: 'Err' }
]
Given a user, for example, John Bar
, we want to re-arrange the list so that John Bar
would move to first in the list, as follow:
const users = [
{ firstName: 'John', lastName: 'Bar' },
{ firstName: 'Jane', lastName: 'Foo' },
{ firstName: 'Jill', lastName: 'Err' }
]
This article is a journey. We will start from a very novice solution in JavaScript. By the end of the journey, we will have a very elegant solution in TypeScript.
Step 1
Our first approach is very naive. We just iterate the array. On each iteration, if we find a match, we just remove the matched element from the array and insert it back to the beginning of the array.
The code in step 1 works but has many problems. The function does not return anything. It takes the argument users
array and mutate it. This makes the function very unwieldy. Inside the function, on each iteration of the forEach
loop, the code mutates the array once with the splice
operation, and then again with unshift
. It’s becoming very hard to keep track of the mutation that happens each time.
To run, simply type
$ node step1
I won’t repeat but you can step 2 through step 5 similarly.
Step 2
In step 2, we will improve over step 2 by making the function returning the output array that has the items re-arranged. The code in step 2 looks like this:
The code starts by declaring an empty array which is used as output. We still use the forEach
loop. On each iteration of the loop, if we find a match, we insert it to the beginning of the output array. Otherwise we just append to the end. The input array to the function no longer gets mutated which is an improvement. But inside the function itself, the forEach
construct requires the output array to be mutated.
Step 3
Step 3 improves over step 2 in that it uses the array method reduce().
On lime 13 and 16, we use the ES6 spread operator to add an element to the accumulator
array on each iteration (much like the forEach
loop), depending if there is a match or not.
We are finally doing functional programming in step 3. The function is a pure function. There is no mutation and no side effect caused anywhere.
Step 4
In step 4, we just use the ternary operator to improve the readability of the code.
Step 5
In step 5, we make the code even more concise since the arrow function only has one statement which is a return
statement. Therefore we can drop the curly braces { }
and the return
keyword.
Step 5 might be as good as we can get if JavaScript is the only option. If TypeScript however is also an option, we can proceed to come up with even a more elegant solution.
Step 6
We take the code in step 5 which is written in JavaScript and rewrite in TypeScript in step 6. Rewriting here simply means we add the types
.
Note that the input array users is marked as readonly
, which prevents it from getting mutated by the function.
If you follow along and want to be able to run, there are a few steps to be done before you can run.
$ npm init -y
The above creates a package.json
file.
$ npm i typescript
The above add typescript
as a dependency to the package.json
file.
Create a file called tsconfig.json
with the following content:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"rootDir": ".",
"moduleResolution": "node",
"outDir": "build",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
Now we can build with the following command:
$ npm run build
The finally run with:
$ node build/step6
Step 7
The above code in step 6 works nicely when we have type called User
which is defined as:
interface User {
firstName: string
lastName: string
}
But what happens when later on we have do the same thing for another type such as Product which is defined as:
interface Product{
name: string
description: string
price: number
}
Duplicate the function findMatchingAndMoveToFirst
to work with another type is hardly a good thing.
So in step 7, we will rewrite using the feature generics in Typescript.
Create a file called findMatchingAndMoveToFirst.ts
with the following content:
The function above, not surprisingly also takes 2 arguments just like previously from step 1 through step 6. The first argument is an array
of some type T
. The second argument is a single object of type U
. The constraint is that the type U
has to implement an interface called Compare
which consists of one function that compares an object of type T
with an object of type U
.
The above paragraph probably doesn’t make much sense when reading it. But everything will become clearer once we look at the code in step 7:
The elegance of the code in step 7 is if we have an array of another type, all we have to do is provide a compare function, then everything else still works.
For example:
Conclusion
That has been quite a journey. We start with a very novice solution. The code in that first step mutates the input data over and over again in many places. It would be too hard to keep track of all the places where data gets mutated.
By the end, in step 7, we have code that works for any kind of array thanks to the power of Generics in TypeScript. In addition, the code is concise, type-safe, declarative and embraces functional programming style.