JavaScript/Typescript
General Practices for JS and TS
Make use of ES6+ features
ES6 (and subsequent updates) added a lot of features to Javascript/Ecmascript with the goal of making it a proper programming language and not something that was written in 10 days. All jokes aside, these updates truly do improve the developer experience quite a lot, and you spend less time fighting with the language and all its hidden caveats, and more on solving whatever you set out to solve.
Some key features introduced in these updates include classes (all hail Java), arrow functions, modules, template literals, let vs const, spread operator, destructuring assignment and Promises. You’ll find more detail on some of these features (and others not mentioned here) in the subsequent sections, along with the reasons to use them.
Var ❌ Let/Const ✅
Declaring a variable with var
, as was done in the olden days of JS, has the potential of introducing side effects. You declare a variable with var
at some point in your code, and then inadvertently reassign it in some place later, leading to embarrassing runtime errors.
Using let
for declaring re-bindable variables, and const
for declaring constants will not only make your code readable, but will also lead to such errors as outlined above being caught at compile (/transpile) time. Prefer using const
when declaring variables unless the variables is to be redefined (or needs to be used in a nested scope). If you think it has to be redefined, think again of some better approach, as the program design that leads to such a situation can often be made better. And if there is no other way, try to keep the part where you are redefining the variable as close to the original declaration as possible.
Arrow Functions
Another great feature introduced by ES6 was arrow functions, which allow us to define a function quite concisely.
Even though we can remove the brackets from the arrow function in case of a single parameter, it is generally better to include the brackets, especially when using Typescript.
One (kinda technical) thing to keep in mind though is that you cannot use this
inside an arrow function. There are multiple reasons for this (one of them being to keep arrow functions ‘pure’) which you can go through using Google or the relevant MDN page.
You’ll find more examples and use-cases of arrow functions in the sections below.
Prefer async-await / Promise API to Callbacks
You might have seen something like this in a tutorial, or some JS/TS program out in the wild. Or you might have heard of the term “Callback Hell”.
You can see how the complexity of such code can increase dramatically. And if we add error handling (onError callbacks), things really get out of hand. Code like this is not only difficult to reason about, but also more difficult to debug, as it doesn’t provide good stack traces in the errors. Imagine staying up the whole night trying to fix the production server, attempting to find the bug by going through the logs, and the error stack trace being all confusing and you banging you head on the wall/table (whichever one you prefer) in frustration. Good error handling would of course help you there, but one core component of good error handling is to make those errors easier to catch (and handle).
A better approach would be to use promises
Or, even better, async-await.
But what if you are working with a third-party package and their API is callback based (it happens quite often). Well, there’s a simple and nifty way to wrap a callback based function with Promises, enabling us to use the Promise API and async-await (whichever convention is being followed in your current project).
You can find more info on async-await and Promises in the resources section.
Use default arguments instead of short-circuiting or conditional statements
Consider the following code.
There are a couple of problems with this piece of code. Firstly, even if the caller knows that name is an optional argument, they would not know the default value for it unless they read the source for it, leading to an unnecessary context jump. Moreover, what if we want to allow (for some reason) passing an empty string for the name. It will be replaced by DEFAULT_SOMETHING_NAME
as it is a falsy value in JS. Most importantly though, the function signature (name: string | undefined)
doesn’t really convey our intention regarding what we’ll do when no value is provided for name. Maybe we’ll replace it with the default value (as is the case above). Maybe we’ll compute a value based on some other parameters or program state. Maybe we’ll issue a command to set the server on fire. Who knows?
A much simpler and better approach would be to use a default argument.
This won’t take care of cases where we want to compute the value for this argument based on some other arguments, but the caller would know whether we are using some default value for this argument (and which value) or not.
String interpolation with Template Literals
Use template literals for formatting strings instead of using concatenation. This not only makes the code more readable but also more performant. As an added bonus, you can evaluate expressions inside the formatting braces, need to focus less on escaping quotes, and are also able to use them as template literal types for a better type system, and with template functions as tagged templates.
”for … of” Loops
Suppose you have to iterate over every element of an iterable (like an array). One way to do that would be:
In such cases, you can either use the forEach
method (but you can’t return from inside the forEach
method, at least not in the way you want to) or you can use the for ... of
syntax.
Null-ish coalescing (’??’)
You might have seen boolean short-circuiting before, usually used to provide a default value in cases where the input is null
or undefined
. The problem with that method is that it would evaluate to use default value not only for null
and undefined
but for all falsy values.
You can fix this kind of problem easily via null-ish coalescing.
You can also use it in conjunction with the assignment operator (x ??= y
), just like you use ||=
and &&=
.
Optional Chaining
Imagine a deeply nested data structure, like a user object. Suppose you want to get the email
and phoneNumber
of the person’s contact info.
The problem though is that the contact property may not be present on the person. Trying to access person.contact.name
in such a case would throw TypeError
.
Developers used to overcome this problem by adding explicit checks, something like var email = person && person.contact && person.contact.email
. You can see how cumbersome it is to perform such checks, and how ugly the code becomes with them. We can perform the same using optional chaining.
Object and Array Destructuring
Destructuring allows you to extract/unpack values from an array/object.
Quite simple. The only thing you’ll need to take care of is making sure the number of elements on the left side of the assignment operator equals the number of elements on the right side (there’s a way around that using the rest
syntax, which you’ll see in the next section).
Much more useful is object destructuring, where you can extract specific properties from an object, even the deeply nested ones.
The names of the variables on the left side of =
in object destructuring must match the names of the right side object’s properties. If you want to name those variables something else, you can do so like this.
One really useful way to use this syntax is in function parameters. Let’s use an example from above:
So far so good. But let’s say you add some other properties to the User
class, and add some more parameters to the constructor. Some of those parameters will be required, others optional.
With this structure though, the calling code will not have as much freedom as it should. Additionally, the caller has to keep in mind the order of the parameters. What if they want to pass the firstName
but not the userName
, or what if they switch the order of firstName
and lastName
. It may be simple for us to remember it, as we just defined the function a few minutes ago, but if your project grows to a significant size, it becomes harder to keep track of things like this. You might say that you can just hover over the function name to see what parameters it takes, and in what order, but that’s extra effort that will pull you out of the problem-solving domain and send you into the “check the syntax” domain. The more times you switch between the two domains (or the more context switches you make), the more difficult it becomes to stay focused on the task at hand.
A better approach for the scenario outlined above would be to accept an object containing the parameters.
That solves the design part of this problem. But the solution still looks a bit ugly. We can use destructuring to make it a bit better.
Much better now. But we can actually simplify it even further by moving the destructuring part directly to the constructor function signature.
Make use of the spread operator and rest
ES6 came with the new ...
(three dots), known as the ‘spread operator’. This operator is used in two main ways:
- Extract/Expand elements from an iterable (like array / string) or an object literal.
- Joining multiple parameters into an array (used mainly in variadic functions)
Let me give you an example.
As you can see, it’s a really easy and concise way to copy the original object. Not only that, it also simplifies merging multiple arrays/objects.
One thing to keep in mind for merging objects using the spread operator is that obj1
and obj2
may have different values for the same keys, or they might have some different value for someProp
key. In this situation, the precedence goes from right to left.
The reason for this precedence is that using ...
actually spreads the object properties / array elements.
Finally, we come to the rest
syntax, used in conjunction with the spread operator and destructuring. Let’s say you want to extract/pop some specific properties/elements out of an object/array. Instead of using the delete
keyword or splice
function, you can accomplish it quite easily like so:
Prefer functional style over imperative style
Try to use pure functions and functional paradigm rather than defining your operations in an imperative array, especially when it comes to iterables like arrays. Using array methods like map
, filter
and reduce
not only allow you to express the business logic in a simpler manner, but also prevents side effects and allows method chaining.
There are a lot of temporary variables we have to create and keep track of, and those variables take up space not only on the machine but also in our brains.
Compare the imperative solution to the functional one below.
Map and Set
Map
and Set
are extremely useful data structures, known usually in most languages as HashMap and HashSet.
Set
allows you to check for presence of an element in a much faster time (theoretically O(1)) than doing the same using Array
. Try using it in places where you want to check the presence of some item a lot of times, or when you are keeping track of collections of items you have processed in some way and therefore don’t want to process again.
Similarly, you can use Map
for checking the presence of a key and fetching the corresponding value if exists. Kinda like the table of contents in a book, which gives you the page number for a given chapter rather than you having to flip the pages one by one and checking if the page is where your queried chapter starts.
Let’s say you want to modify the data of some users, and the only common property in the new and old data is the Id
. Instead of going through the whole collection/array again and again for each new data point in a nested loop, you can use a Map
.
Named capture groups
Let’s start with a quick recap of capture groups in regular expressions. A capture group is a part of the string that matches a portion of regex in parentheses.
Regular expressions have also supported named capture groups for quite some time, which is a way for the capture groups to be referenced by a name rather than an index. With ES9, this feature made its way to JavaScript.
Combine that with destructured assignment to instantly gain code style points.
Effortless Concurrency with Promise.all
You will probably come across a situation where you are making HTTP calls to multiple external sources. Rather than doing these operations one by one and wasting our time waiting for network bound results, we can make these calls concurrently (not in parallel, looks similar but is an entirely different).
Promise.all
can be used not only for network bound concurrent operations but also for concurrent I/O bound operations. The only requirement is that one operation should not be depending on the result of another. Otherwise, we can really perform the dependent operation before its dependency has finished execution.