Skip to main content

JavaScript -> TypeScript Style Guide

info

Given the positive experience with TypeScript while writing the VS Code xPack extension, starting in 2021, existing JavaScript projects will be migrated to TypeScript.

Standard TS/JS

After many years of working with style guides for various languages, the conclusion is that the specific style is of lesser importance; what matters more is its consistent application.

To ensure consistency in the xPack JavaScript/TypeScript source files, the primary requirements are:

Following this, the main recommendations are:

  • utilise the ECMAScript 6 specifications (ES 6)
  • asynchronous functions should use promises (and definitely avoid callbacks)
  • reentrancy should be carefully considered (avoid module-global variables)

The xPack Project preferences

Prefer TypeScript over JavaScript

This is Rule No. 1, which overrides all other rules.

If JavaScript code must still be used in some instances, prefer ES6 solutions.

Definitely avoid using old-style code.

Use classes as much as possible

Even if the new syntax is mostly syntactic sugar, and internally things behave as they did in the first JavaScript versions, still use the new class syntax extensively; it is much cleaner and improves readability.

Use promises instead of callbacks

Really. No callbacks at all. Use promises. Actually use async/await.

Use async/await for asynchronous calls

Once async/await became standard, and the V8 engine added support for them, there is no reason for not using async/await.

Wrap old legacy code using callbacks into promises and execute them with await.

Use static class members for sharing

Modules are singletons; using module variables is like using static variables in a multi-threaded environment; they may provide a way of sharing common data between multiple instances of objects created inside the same module, but if not handled correctly this may have unexpected results.

The general recommendation is to make the modules re-entrant. In practical terms, do not use module-global variables at all; make the module export a class, and create instances of it whenever needed; for sharing data between instances, use static class members.

Do not restrict exports to a single function or class

Bad style:

module.exports = function () {
return /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
};
const func = require('name')

Apart from being unnamed, returning a single function prevents future extensions, for example exporting a second function from the same module would mandate all modules that use the first function via require() to be updated to require().func1, which may cause many headaches to developers.

Recommended style:

export function func1 () {
return /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g
}
export function func2 () { ... }
import { func1, func2 } from 'name'

The recommendation is to always export functions or preferably classes and get them individually by name.

Prefer static classes to group methods

Prepare your module to export multiple functions; group them (by functionality) either below a parent object, or, even better, in classes with static members.

The main advantage of this scheme is that adding new exports will only change the interface incrementally, minimising the risk of breaking backward compatibility.

Use the spread operator

The spread operator expands an iterable into its member objects. It also works with arrays and objects

  • Create array copies
const arr1 = [1, 2, 3]
const arr2 = [...arr1]
  • Concatenate arrays
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const arr3 = [...arr1, ...arr2]
  • Enumerate object properties
class Base {
constructor (args) {
super({
name: 'tree',
parent: null,
...args
})
// ...
}
}

Iterate over an Array

For very simple loops, (one-two lines), use forEach():

const iterable = [10, 20, 30];

iterable.forEach((value) => {
console.log(value)
})

Debug note: Visual Studio Code is able to step into the lambda function.

For more complex loops, use for ... of (not in!).

let iterable = [10, 20, 30]

for (const value of iterable) {
console.log(value)
}
// 10
// 20
// 30

If the value needs to be changed, use let:

let iterable = [10, 20, 30];

for (let value of iterable) {
value += 1
console.log(value)
}
// 11
// 21
// 31

If the last value of the variable is needed outside the loop, define the variable before the loop:

let value
for (value of iterable) {
// ...
}
console.log(value)

Do not use for ... in since it iterates over the enumerable properties, which include inherited properties (use .hasOwnProperty() to filter them out).

Iterate over the keys of an Object

If the order is not important, iterate over the keys or the entries:

// array like object with random key ordering
var anObj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.keys(anObj)); // ['2', '7', '100']
console.log(Object.entries(anObj)); // [ ['2', 'b'], ['7', 'c'], ['100', 'a'] ]

for (const [key, value] of Object.entries(anObj)) {
console.log(key, value)
}
// 2 b
// 7 c
// 100 a

Check if object is Array

Array.isArray([1, 2, 3]);  // true

MDN

Check if object is String

isString (x) {
return typeof value === 'string'
}
note

The previous recommandation was to use Object.prototype.toString.call(x) === '[object String]', needed to distinguish between primitive strings ('hello') and String objects (new String('hello')), but String objects are rarely used in modern JavaScript and are generally considered an anti-pattern.

Check if object

isObject (x) {
return typeof x === 'object' && !Array.isArray(x)
}

Please note that null is also an object, and everything created with new is also an object, including:

  • new Boolean(true)
  • new Number(1)
  • new String('abc')

MDN

Use Map to guarantee the order of the values

Although maps can be conveniently implemented with regular objects, the specifications do not guarantee the insert order to be preserved.

If the order is important, or if the object needs to store other properties too, use a Map.

Call the class method via the prototype

If a method of a class is overridden, calling it from the base class actually calls the derived method (run-time polymorphism).

To ensure the local function, use the function prototype:

class Base {
constructor () {
Base.prototype.clear()
}

clear () {
// ...
}
}

Destructured parameters

For functions with more than 1 parameter, pack them in an object:

class Base {
/**
* @params name The object name.
* @params type The object type
* @returns Nothing
*/
f1 ({
name,
type
}: {
name: string
type: string
}): void {
assert(params.name)
console.log(params.name)
// ...
}

f2 () {
f1 ({
name: value1,
type: value2
})
}
}

Use copy/move constructors & support methods

Make the constructor accept a from parameter, to use it as a source when creating the object.

The default behaviour is to make a copy of the original object.

Add a doMoveAll: true to instruct a move operation; be sure original references are cleared (set to undefined).

Alternatively use copyFrom(from) and possibly appendFrom(from).

Use a clear() method to clear the object content (it might also be useful during testing).

Use the # prefix for private members.

According to recent standard updates, names starting with # are private.

Use the _ prefix for private members.

Similarly, although this is not part of any standard, use names starting with _ for the protected members.

Use a separate location for cached variables

If the object uses local cached objects, group them below a _cache object. Initialise it to an empty object in the constructor and in the clear() method.

No need to end the name of the variables with _.

class Base {
constructor () {
this._cache = {}
}

clear () {
this._cache = {}
}
}

Self

When a reference to the static methods or variables is needed, to make things explicit, prefer to define a Self variable.

From within regular methods, use:

// Explicit uppercase, to be obvious when a static property/method is used.
const Self = this.constructor

From within other static methods, use:

// Explicit uppercase, to be obvious when a static property/method is used.
const Self = this

From Understanding ECMAScript 6

The main specifications to be followed are those of ES 6; they override all other older specifications and style guides.

Block bindings

Use const by default, and only use let when you know a variable’s value needs to change

Functions

Use default parameters.

const makeRequest = function (url, timeout = 2000, callback = function() {}) {
// the rest of the function
}

const add = function (first, second = getValue(first)) {
return first + second
}

Use rest parameters.

const pick = function (object, ...keys) {
let result = Object.create(null)
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]]
}
return result
}

The Function constructor.

var add = new Function("first", 'second = first',
'return first + second')
console.log(add(1, 1)) // 2
console.log(add(1)) // 2

JavaScript has two different internal-only methods for functions: [[Call]] and [[Construct]]. When a function is called without new, the [[Call]] method is executed, which executes the body of the function as it appears in the code. When a function is called with new, that’s when the [[Construct]] method is called. The [[Construct]] method is responsible for creating a new object, called the instance, and then executing the function body with this set to the instance. Functions that have a [[Construct]] method are called constructors.

const Person = function (name) {
if (this instanceof Person) {
this.name = name
} else {
throw new Error('You must use new with Person.')
}
}
var person = new Person('Nicholas')
var notAPerson = Person.call(person, 'Michael') // works!

Block-level functions

'use strict'

if (true) {
console.log(typeof doSomething) // throws an error
let doSomething = function () {
// empty
}
doSomething()
}
console.log(typeof doSomething)

Arrow functions are functions defined with a new syntax that uses an arrow (=>)

  • No this, super, arguments, and new.target bindings The values of this, super, arguments, and new.target inside the function are defined by the closest containing non-arrow function
  • Cannot be called with new Arrow functions do not have a [[Construct]] method and therefore cannot be used as constructors. Arrow functions throw an error when used with new.
  • No prototype Because you can’t use new on an arrow function, there’s no need for a prototype. The prototype property of an arrow function doesn’t exist.
  • Can’t change this The value of this inside the function can’t be changed. It remains the same throughout the entire life cycle of the function.
  • No arguments object Because arrow functions have no arguments binding, you must rely on named and rest parameters to access function arguments.
  • No duplicate named parameters Arrow functions cannot have duplicate named parameters in strict or non-strict mode, as opposed to non-arrow functions, which cannot have duplicate named parameters only in strict mode.
let sum = (num1, num2) => num1 + num2
// effectively equivalent to:
let sum = function(num1, num2) {
return num1 + num2
};

Enhanced Object Functionality

It is now possible to modify an object’s prototype after it has been created thanks to ECMAScript 6’s Object.setPrototypeOf() method.

In addition, you can use the super keyword to call methods on an object’s prototype. The this binding inside a method invoked using super is set up to automatically work with the current value of this.

From npm's "funny" coding style

As the name implies, some of them are funny, but some are still useful.

Line length

Keep lines shorter than 80 characters

Indentation

Indentation is two spaces

Curly braces

Curly braces belong on the same line

const f = function () {
while (foo) {
bar()
}
}

Semicolons

Don't use semicolons, except when required; for example to prevent the expression from being interpreted as a function call or property access, respectively.

;(x || y).doSomething()
;[a, b, c].forEach(doSomething)

Comma first

Put the comma at the start of the next line, directly below the token that starts the list

const magicWords = [ 'abracadabra'
, 'gesundheit'
, 'ventrilo'
]
, spells = { 'fireball' : function () { setOnFire() }
, 'water' : function () { putOut() }
}
, a = 1
, b = 'abc'
, etc
, somethingElse

Quotes

Use single quotes for strings except to avoid escaping

const ok = 'String contains "double" quotes'
const alsoOk = "String contains 'single' quotes or apostrophe"
const paramOk = `Back-quoted string with ${parameter}`

White spaces

Put a single space in front of ( for anything other than a function call

Functions

Use named functions.

  • always create a new Error object with your message (new Error('msg'))
  • logging is done using the npmlog utility

Case, naming, etc

  • use lowerCamelCase for multi-word identifiers when they refer to objects, functions, methods, properties, or anything not specified in this section
  • use UpperCamelCase for class names (things that you'd pass to "new")
  • use all-lower-hyphen-css-case for multi-word filenames and config keys
  • use named functions, they make stack traces easier to follow
  • use CAPS_SNAKE_CASE for constants, things that should never change and are rarely used

null, undefined, false

  • boolean variables and functions should always be either true or false
  • when something is intentionally missing or removed, set it to null
  • don't set things to undefined
  • boolean objects are verboten

Exceptions

For the native Node.js callback usage, throwing anything is worse than useless. Simply send the error message back as the first argument to the callback.

However, for the modern ES 6 promise usage, exceptions are acceptable.

From Node.js modules

Global objects

These are really objects, available in all modules. (see Node.js Globals)

  • global
  • process
  • console
    • console.log('msg') - writes 'msg' to stdout
    • console.warn('msg') - writes 'msg' to stderr
    • console.error('msg') - writes 'Error: msg' to stderr
    • console.assert(value, 'msg')

The Spread syntax

According to JavaScript ES6 — The Spread Syntax (…):

  • The spread syntax is simply three dots: ...
  • It allows an iterable to expand in places where 0+ arguments are expected.

Useful cases:

  • inserting Arrays
var mid = [3, 4];
var arr = [1, 2, ...mid, 5, 6];

console.log(arr);
  • Math
var arr = [2, 4, 8, 6, 0];
var max = Math.max(...arr);

console.log(max);
  • Copy an Array
var arr = ['a', 'b', 'c'];
var arr2 = [...arr];

console.log(arr2);
  • String to Array
var str = "hello";
var chars = [...str];

console.log(chars);

Reformatting and Validation Tools

Other links: