7 Steps to Better JavaScript Development

Mar 27, 2018 23:53 · 2612 words · 13 minute read

JavaScript is everywhere - from classic frontend development for the browser over server-side applications (Node.js), you can now use it to develop desktop (Electron) and mobile (React Native) applications. People use it for everything - command-line tools (e.g. Heroku CLI), enhancing websites with userscripts, scripting in games like 0 A.D., and even for developing crypto-currency traders!

The problem: It is very easy to write bad JavaScript code. Not only is it easy to write spaghetti code, but, because JavaScript has no compilation phase, even the simplest errors like a missing bracket are by default not discovered until actually running the program.

In this post, I want to give an overview on what I concretely do to do a good job in programming JavaScript, and keep my code quality high. Most of the tips I give here are not limited to JavaScript, but can be applied as good practice for software development in general.

The steps to better code quality are ordered by importance - don’t try to implement every step at once - but do it gradually and depending on the needs of your business. Don’t forget: Getting things done is more important than high code quality!

Step 1: Use a linter

A linter helps developers discover problems with their code without executing it - problems like the missing bracket mentioned before can be easily found and highlighted. But it doesn’t stop there!

The most popular linter for JavaScript development is ESLint. It can help you in different problem categories: First, possible errors like using an assignment operator in a conditional statement:

// possible assignment error (x will have value 0 afterwards, did you really mean that?)
if (x = 0) { ... }

Then, it can highlight stylistic issues like indentation and therefore help you maintain a consistent code formatting:

if (x === 0) {
// indentation error
return "a";
}

My favorite feature is the suggestion of Best Practices. ESLint actively helps you learn better ways of doing something.

A frequent code smell in JavaScript is the use of the == and != comparison operators, e.g. a == b. What many developers coming from languages like Java don’t know: These comparisons are not type-safe. If a and b have the same type (e.g. number), everything works as expected. But if they don’t, JavaScript tries to convert them to the same type, which can cause confusion.

Would you have thought that 3 == "03" results in true? Therefore, you should always use the strict equality operators === and !== - ESLint reminds you of that.

ESLint supports a lot of rules, but doesn’t force any of them on you by default. Instead, during the initialization process, you can either activate a recommended set of rules or use one of the style guides provided by the community. I prefer the one by Airbnb - it leads to a modern code style & promotes new powerful language features.

For getting problems in your code highlighted in your favorite code editor, you should install a package, e.g. SublimeLinter for Sublime Text 3. Also, you should know about the nice --fix option in the ESLint command line interface, which can fix problems for you, mainly of stylistic nature.

Step 2: Make your editor help you more

If you come from Java, you will most likely have worked with an IDE like IntelliJ or Eclipse - they provide useful powerful auto-complete suggestions while you type, e.g. displaying the parameters a method needs or the properties an object has.

In JavaScript world, you want that as well!

If you want it easy: Either use a full-blown IDE like WebStorm (free for students) or use Visual Studio Code (free). These two provide good JavaScript auto-complete support out of the box.

The two main competitors for web development are Sublime Text and Atom. There are valid reasons for still using them, e.g. the speed of Sublime and the extendability for Atom. Getting good auto-complete support is just a bit harder.

For both editors, the solution is tern, a JavaScript code analyzer that helps the editors generating better auto-complete suggestions. Unfortunately, the tern project is not actively maintained anymore, but it should be good-enough for most use-cases.

For Atom, install the packages autocomplete-plus and atom-ternjs. For Sublime, I use tern_for_sublime. After installation, I recommend to modify your configuration a bit, especially activating tern_argument_hints and tern_argument_completion and extending your Sublime auto_complete_triggers.

Step 3: Use a language reference

Using Google is one of the most frequent activities in a developer’s day. While this is often the right thing to do, if you want to know how to use a specific JavaScript method or want to get an overview of which features the language offers, heading to Google and reading through various websites with varying quality will make you loose time.

Instead, use a good JavaScript reference - the best is the MDN web docs JavaScript reference by Mozilla. Here, you can find deep information about everything JavaScript has to offer for you, including exact method parameters & return values, demos, examples, browser compatibility and information about experimental features.

MDN is awesome - use it with an API documentation browser app to search through it more easily, faster and with offline-support. I use Dash (paid, macOS only), a free alternative would be the DevDocs web app. In both apps, you then then e.g. just type “array.” and instantly get a list of available methods on array objects.

Step 4: Write tests

Writing tests should be a no-brainer and giving an introduction into JavaScript testing is not in the scope of this post. My cup of tea: For backend applications, I use mocha with chai, for frontend applications I use whatever the frontend framework I’m using suggests.

I would argue that frontend testing is less important, at least as long as you don’t have much logic in your frontend. Testing your code logic is important, testing your UI with tools like the browser-automation tool Selenium is almost always an overkill.

Some general words about testing:

In my opinion, the two greatest advantages of writing tests are:

  1. Confidence in code: After refactoring your code or adding new features, you still know that your old code is working as expected
  2. Perspective-switch: You are forced to take the perspective of a user of your code - this will enable you to rethink your structure and make life better for other developers depending on your code

When writing tests, remember that you can test on different layers of your application, from lower levels (testing methods) to higher layers (testing server responses). If you are of the opinion that testing your code is not possible at all, this is usually a sign that your method or feature should be refactored.

Testing individual methods corresponds to Unit Testing. Writing unit tests for every method is tedious and often not worth it, e.g. if a method is really short and simple (which should be the goal for every method). That said, every method should still be written in a style that adding tests for it would be easy.

It is more important to test the upper layers of your applications, e.g. if your backend responds correctly to certain queries, where the response to the query is generated by multiple methods working together. This type of test is called Integration Testing.

Integration tests give you information about the big picture and typically don’t need to be maintained as much as unit tests. Focus on writing integration tests, write unit tests only for your most critical methods.

Step 5: Strive for pure methods & functional programming

One of the most difficult and crucial tasks in developing an application is managing the application’s state. One typical anti-pattern that complicates state management: When a method doesn’t return the same value, although the same input was provided.

To illustrate this scenario, imagine the following code:

let x = 3;

function getResult(a) {
  if (x === 3) {
    return a;
  }
  throw new Error();
}

[...more code...]

Now, when called, getResult(100) will mostly return 100. But when another part of the code sets x to a different value than 3, the exact same call will throw an exception.

Especially annoying: Although the exception gets thrown when getResult gets called again, but x could have been modified a long time before that. Therefore, it is quite hard to find out who caused x to change. Testing and debugging the flow of your application becomes very hard.

That’s why, in your code, you should strive for pure functions. Pure functions have two characteristics:

  1. With the same input, a pure function always returns the same output. From this follows: A pure function’s output value shouldn’t depend on anything that may change during the programs execution. It should just depend on its input and the program’s constants.
  2. A pure function doesn’t have any side effects, e.g. it doesn’t change any of the program’s variables.

The scenario from before wouldn’t have been possible with getResult being a pure function. Pure functions make reasoning about a program’s state easier.

In general, pure functions are one of the key concepts in functional programming. As a developer, it is very beneficial to know about this style of programming and get familiar with pure functional programming languages like Haskell or Elm. Why? By applying functional programming principles, certain types of errors just disappear or become much easier to test and debug.

It is possible to program JavaScript in a functional way, but because functional programming is not the sole way of doing things here, better first get some experience with languages that force you to think functionally. After that, you can get back to JavaScript!

As an appetizer, here are some key JavaScript functions you’ll need when programming in a functional way:

// Calls the function on every array element and returns the results as a new array.
arr.map(fun)
// Returns a new array with the values of the array where the function returned true.
arr.filter(fun)

// Only returns true if the function returns true for every element of the array.
// Otherwise returns false
arr.every(fun)
// Returns true if the function returns true for at least one element of the array.
// Otherwise returns false
arr.some(fun)

// with {} as target value - to clone and merge several source objects into a new object.
Object.assign(target, ...sources)

 // Returns an array of an object's properties
Object.keys(obj)
// Returns an array of an object's values
Object.values(obj)

Don’t forget, you can (and should) look up the exact method signatures in the MDN JavaScript documentation.

Step 6: Disallow deployment on Linter or Test errors

This step acts like a thumbscrew for steps 1 (Use a linter) and 4 (Write tests). If you use software like Continuous Integration (CI) or Continuous Deployment (CD), of which you should probably use at least CI at some point, you can let your server run linter checks & tests automatically for you.

In case there are linter errors in some of the files you have changed, or some of the tests are now breaking, the server could then disallow your changes to go into the main branch of your application development. This really reminds & forces you to keep a high level of code quality.

But don’t implement this step too early - a pedantic server like this will make you move a bit slower. This change really is just needed for larger teams where not everyone might have a high incentive to keep a high code quality standard or breaks stuff out of laziness without running the tests locally.

Step 7: Add static typing

The last step, and also one of the bigger changes for existing code-bases, is introducing static types to our JavaScript via TypeScript (by Microsoft) or Flow (by Facebook). These tools allow you to add type annotations to your code which help your editor to give you feedback about your code.

On the one hand, your editor can give you even better suggestions, e.g. it won’t suggest a variable holding a string as a parameter for a method requiring a number, and uncovers many errors - think of the ESLint “possible errors” category on steroids.

Here an example of an error that can be prevented by using type annotations:

function multiplyByTwo(x) {
    return x * 2;
}

console.log(multiplyByTwo(2)); // will log "4"
console.log(multiplyByTwo("2")); // will also log "4"!?
console.log(multiplyByTwo("blub")); // will log "NaN"

Normally, here, you wouldn’t expect multiplyByTwo to be able to handle any strings at all - but now, it returns a “correct” value for some strings, while, for others, it returns NaN. With a type annotation, you can explicitly define number as your expected input type and trigger an error if the type is violated:

function multiplyByTwo(x: number) { // type annotation here
    return x * 2;
}

 // will log "4"
console.log(multiplyByTwo(2));
// error, already before actually running the script:
// Argument of type '"2"' is not assignable to parameter of type 'number'.
console.log(multiplyByTwo("2"));
// error, already before actually running the script:
// Argument of type '"blub"' is not assignable to parameter of type 'number'.
console.log(multiplyByTwo("blub"));

On the other hand, your editor can offer you more powerful refactoring tools and makes you more confident that a refactoring was successful. This makes it especially useful for larger projects.

In the end, runtime errors are much less likely. It almost feels like you are using a language with a built-in type system like Java. Behind the scenes, after the type-checks, both tools compile your typed code to standard JavaScript code that can be executed by your JavaScript runtime (e.g. the browser).

TypeScript and Flow are quite similar in features and syntax. I have used both - TypeScript in a university project and Flow at Facebook - and cannot say anything bad about either of them. I got the feeling that TypeScript has gained more traction; it is used in Angular, and has prominent users such as Slack for their desktop app. That’s why I would probably use TypeScript if I were to start a new project now.

Both TypeScript and Flow don’t require your whole codebase to be typed. Theoretically, you can just plug them in and your code should still work as before. You can then gradually add type annotations and enjoy the improved editor capabilities.

It is worth noting that you have to change your linter configuration as you are not writing standard JavaScript code anymore. For Flow, there exists eslint-plugin-flowtype to make ESLint compatible with flow. For TypeScript, there exists the TSLint linter tool by Palantir as an ESLint replacement.

Again, think twice before going all-in with TypeScript or Flow - they are most useful with larger projects. For smaller projects, especially if you are not familiar with the tool yet, it could make you loose focus of developing your actual application and slow you down.

That’s all? Of course it isn’t!

Of course these steps neither will wonderfully make your actual product appear out of nowhere nor will you automatically have top-notch software quality. There are many more areas to focus and improve on.

Besides many more things on the technical side (e.g. learn to use the debugger, learn to split up your code into smaller easy-to-understand methods, learn to document your code in an appropriate way), many words could be spent on the non-technical, soft skill side of the game: Learn how to identify and prioritize your product’s customer requirements, estimate your tasks, and set up a development process where every developer in your team can be productive and continuously improve.

But these are topics for another post. I hope you enjoyed this list of steps on how to improve your JavaScript development!