Tips to Improve JavaScript Coding
In this article, we are listing tips that may help you improve your JS coding processes.
Use TypeScript
Ironically, the best thing to improve your JS code writing is not to write JS. TypeScript (TS) is a compiled JS, meaning that everything executed in JS is completed in TS. Thus, TS code can be used for developing any application for any browser. Moreover, you can choose a JS version where the code will be compiled.
TS adds a comprehensive additional typing system on top of general JS. TS support in the ecosystem has been somewhat inconsistent for a long time. Fortunately, those days have passed, and most frameworks support TS by default.
Reasons to use TS
TS provides “types safety”
Types safety refers to the processes by which the compiler verifies that all types are used in a “proper” way throughout a piece of code. In other words, if you create a function foo
that takes a number:
function foo(someNum: number): number { return someNum + 5; }
This foo
function should be used with a number only:
console.log(foo(2)); // prints "7"
This one is not appropriate:
console.log(foo("two")); // invalid TS code
There are no disadvantages of type safety aside from the consumption of adding types to your code. On the other hand, the benefit is too great to be ignored. Type safety provides an additional layer of protection against common errors, which is good for a language like JS.
Types of machine-written texts make larger applications refactoring possible
The process of refactoring a large JS application may become a real nightmare. Most of the problems with JS refactoring are caused by the fact that it doesn’t support function signatures. This means that a JS feature can never be “misused”. For example, if we have a myAPI
function that is used by a thousand of different services:
function myAPI(someNum, someString) { if (someNum > 0) { leakCredentials(); } else { console.log(someString); } }
And we change it a little:
function myAPI(someString, someNum) { if (someNum > 0) { leakCredentials(); } else { console.log(someString); } }
We must be 100% sure that the usage is updated properly in every location where this feature is used (1000 locations). If we even miss 1, the credentials might be leaked. Here’s the same scenario with TS:
before
function myAPITS(someNum: number, someString: string) { ... }
and after
function myAPITS(someString: string, someNum: number) { ... }
You may have noticed that the myAPITS
function has been changed the same way as its JavaScript equivalent. But instead of being coerced into correct JavaScript, this code results in invalid TypeScript, as thousands of places where it is used now provide the wrong types. And because of the “types safety” mentioned above, these 1000 cases will block compilation, and your credentials will not be skipped.
TypeScript eases communication in a command architecture
When TS is configured correctly, it isn’t easy to write code without defining interfaces and classes first. However, it also provides an opportunity to share brief, communicative architecture suggestions. Other solutions to this problem existed before TS, but none of them solved it in the first place or forced you to do additional work. For example, if you want to suggest a new Request
type for your backend, you can send the following using TS:
interface BasicRequest { body: Buffer; headers: { [header: string]: string | string[] | undefined; }; secret: Shhh; }
Overall, TS has evolved into a mature and more predictable alternative to JS. There is definitely still a need for JS usability. However, new projects are mostly TS from the very start.
Use modern functions
JavaScript is one of the most popular programming languages in the world. Many changes and additions to JS lately (technically, the ECMAScript) have revolutionized the developer experience.
For a long time, event-driven asynchronous callbacks have been an inevitable part of JS development:
the traditional callback
makeHttpRequest('google.com', function (err, result) { if (err) { console.log('Oh boy, an error'); } else { console.log(result); } });
To resolve the callbacks issue, the new “Promise” concept has been added to JS. Promises allow you to write asynchronous logic while avoiding the nesting issues that previously plagued callback-based code.
Promise
makeHttpRequest('google.com').then(function (result) { console.log(result); }).catch(function (err) { console.log('Oh boy, an error'); });
The most significant advantage of Promises over callbacks is readability and chaining.
While promises are a good decision, they still leave a lot to be desired. To fix this, the ECMAScript committee decided to add a new method for using async
and await
promises:
async
and await
try { const result = await makeHttpRequest('google.com'); console.log(result); } catch (err) { console.log('Oh boy, an error'); }
the required definition of makeHttpRequest
in the previous example
async function makeHttpRequest(url) { // ... }
It is also possible to await
a Promise directly since the async function is just a wrapper for a Promise. This also means that the async
/await
code and the promise code are functionally equivalent.
let
and const
For most of JS’s existence, there was only one variable scope qualifier, var
. var
has some pretty unique/interesting rules about how it processes visibility area. The var
scoping behavior is inconsistent and confusing and has resulted in unexpected behavior and hence bugs throughout the lifetime of JS. But as far as ES6 is concerned, there are alternatives to var
. These are const
, and let
. There is almost no need to use var
. Any logic that uses var
can always be converted to equivalent const
and let
code.
As for using whether const
or let
, start by declaring everything const
. const
is much stricter and “immutable”, which usually results in better code. There aren’t many “real-world scenarios” where let
is required.
Arrow =>
functions
Arrow functions are a quick method for declaring anonymous functions in JS.
Anonymous functions describe functions that are not explicitly named. Usually, anonymous functions are passed as a callback or event handler.
vanilla anonymous function
someMethod(1, function () { // has no name console.log('called'); });
For the most part, there is nothing “bad” about this style. Vanilla anonymous functions behave “interestingly” in terms of scope, leading to many unexpected errors. We don’t have to worry about that anymore, thanks to the arrow functions. Here’s the same code implemented with an arrow function:
anonymous arrow function
someMethod(1, () => { // has no name console.log('called'); });
Aside from being more concise, arrow functions also have much more practical scope behavior. Arrow functions inherit this
from the scope in which they were defined.
In some cases, arrow functions can be even more brief:
const added = [0, 1, 2, 3, 4].map((item) => item + 1); console.log(added) // prints "[1, 2, 3, 4, 5]"
Arrow functions on the same line include an implicit return statement. No need for parentheses or semicolons for single-line arrow functions.
This is not the same var
situation, there are still valid use cases for vanilla anonymous functions (class methods in particular). That being said, if you always use the default arrow function, you end up doing a lot less debugging than the default vanilla anonymous functions.
Spread operator ...
Extracting key/value pairs from one object and adding them as children of another object is a very common scenario. Historically, there have been several ways to do this, but they are all rather clumsy:
const obj1 = { dog: 'woof' }; const obj2 = { cat: 'meow' }; const merged = Object.assign({}, obj1, obj2); console.log(merged) // prints { dog: 'woof', cat: 'meow' }
This template is incredibly common, so the approach described above quickly becomes tedious. Thanks to the “spread operator”, you never need to use it again:
const obj1 = { dog: 'woof' }; const obj2 = { cat: 'meow' }; console.log({ ...obj1, ...obj2 }); // prints { dog: 'woof', cat: 'meow' }
This works with the arrays, too
const arr1 = [1, 2]; const arr2 = [3, 4]; console.log([ ...arr1, ...arr2 ]); // prints [1, 2, 3, 4]
Template literals (template strings)
Strings are one of the most common programming constructs. This is why it’s so embarrassing that native string declarations are still poorly supported in many languages.
Template literals natively and conveniently solve two of the biggest problems with writing strings, adding dynamic content, and writing strings that span multiple lines:
const name = 'Ryland'; const helloString = `Hello ${name}`;
Destructuring assignment
Object destructuring is a way to retrieve values from a collection of data (object, array, etc.) without having to iterate over the data or explicitly access its key:
function animalParty(dogSound, catSound) {} const myDict = { dog: 'woof', cat: 'meow', }; const { dog, cat } = myDict; animalParty(dog, cat);
You can define destructuring in the function signature:
function animalParty({ dog, cat }) {} const myDict = { dog: 'woof', cat: 'meow', }; animalParty(myDict);
This works with the arrays, too:
[a, b] = [10, 20]; console.log(a); // prints 10
Always assume that your system is split
Writing concurrent applications, your goal is to optimize the amount of work you do in one go. If you have four available cores and your code can only use one core, 75% of your potential is wasted. This means that blocking synchronous operations is the main enemy of parallel computing. But given that JS is a single-threaded language, it doesn’t work on multiple cores. So what’s the point?
JS is single-threaded but not single-filed. It can take seconds or even minutes for the HTTP request to be sent; if the JS stops executing the code until a response from the request is received, the language will be unusable.
JavaScript solves this problem with an event loop. The event loop iterates over logged events and executes them based on internal scheduling/prioritization logic. This is what allows you to send thousands of “concurrent” HTTP requests or read multiple files from disk at the “same time”. Here’s the catch: JavaScript can only take advantage of this feature if you use the correct functions. The simplest example is a for
loop:
let runningTotal = 0; for (let i = 0; i < myArray.length; i += 1) { if (i === 50 && runningTotal > 50) { runningTotal = 0; } runningTotal += Math.random() + runningTotal; }
This code only produces the desired result if it is executed in order, iteration by iteration. If you tried to run multiple iterations simultaneously, the processor might incorrectly branch based on approximate values, invalidating the result. If this were C code, we would have a different conversation since its use is different, and there are quite a few tricks that the compiler can do with loops. In JavaScript, traditional for
loops should only be used when necessary. Otherwise, use the following constructs:
map
// in decreasing relevancy :0 const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com']; const resultingPromises = urls.map((url) => makHttpRequest(url)); const results = await Promise.all(resultingPromises);
map with index
// in decreasing relevancy :0 const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com']; const resultingPromises = urls.map((url, index) => makHttpRequest(url, index)); const results = await Promise.all(resultingPromises);
forEach
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com']; // note this is non blocking urls.forEach(async (url) => { try { await makHttpRequest(url); } catch (err) { console.log(`${err} bad practice`); } });
Rather than doing each “iteration” in order (sequentially), constructs such as map
take all the elements and send them as separate events to a user-defined map function. This directly tells the runtime that the individual “iterations” are not related or dependent on each other, which allows them to run concurrently. In many cases, a for loop will perform just as much (and maybe even more) than a map
or forEach
loop. Losing a few loops is now worth the benefit of using a well-defined API. Thus, any future improvements to the implementation of these data access patterns will benefit your code. The for loop is too general to have meaningful optimization for the same pattern.
There are other valid async parameters aside from map
and forEach
, such as for-await-of
.
Follow the style
Code without a consistent style (appearance) is incredibly difficult to read and understand. Hence, a critical aspect of writing high-quality code in any language is having a consistent and sane style. Due to the breadth of the JS ecosystem, there are many linter options and styling features.
Many people ask if they should use eslint
or prettier
. They serve completely different purposes, so they should be used together. Eslint
is a traditional linter, and in most cases, it detects problems with your code that are not so much related to style as to correctness. For example, the following code will crash the linter:
var fooVar = 3; // airbnb rules forebid "var"
Prettier is a code formatting program. He is less concerned with “correctness” and much more concerned with uniformity and consistency. Prettier won’t complain about using var
, but it will automatically align all parentheses in your code. In the development process, many developers do prettier as the last step before submitting code to Git. In many cases, it makes sense to even automatically start Prettier every time you commit the repository. This ensures that all code going into source control is consistent in style and structure.
Test your code
Writing tests is an indirect but incredibly effective method of enhancing the JS code you write. Your testing needs will vary, and there is no single tool that can handle everything. There are many well-established testing tools in the JS ecosystem, so the choice of tools is mostly down to personal taste. As always, think for yourself.
AvaJS
Test drivers are simply frameworks that provide structure and utilities at a very high level. They are often used in conjunction with other specific testing tools that differ depending on your testing needs.
Ava is the right balance of expressiveness and conciseness. Faster tests save developers time and companies money. Ava boasts a lot of nice features like inline assertions while staying minimal.
Alternatives: Jest, Mocha, Jasmine.
Sinon
Spies provide us with “functional analytics” such as how many times a function was called, how they were called, and other useful data.
Sinon is a library that does a lot of things, but only a few very well. In particular, Sinon excels when it comes to spies and stubs. The feature set is rich, but the syntax is concise. This is especially important for stubs, as they are partly there to save space.
Alternative: testdouble
Web Automation – Selenium
Since it is the most popular web automation option, it has a huge community and a set of network resources. Unfortunately, the learning curve is pretty steep, and for real use, it depends on a lot of external libraries. That being said, it’s the only genuinely free option, so if you’re not into any enterprise-grade web automation, Selenium will get the job done.