Tips to Improve JavaScript Coding

#coding #javascript #js

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.

 

 

 

 

 

 

 

 

 

 

 

 

Previous Topic
Useful GitHub Repositories
Next Topic
Main Approaches of Data Analysis
We Love to Hear From You
For any support, sales, careers, or security inquiry please contact us!

    * - marked fields are required.