Skip to content

TypeScript Guidelines

David Zearing edited this page Nov 27, 2018 · 7 revisions

Coding Guidelines

General design principles

Single responsibility

Module and classes should only have a single purpose. It is a code smell when we have modules with a lot of code in it.

Having a single purpose makes the code more easily testable. Often complex classes which do many things will retain internal state that makes things difficult to mock or to cover every permutation.

Favor composition over inheritance

Even as the ES6 Javascript standard is giving us the "class" keyword, we should stay away from inheritance as a model for extending functionality. Inheritance forces a single linear path within which to extend functionality. Functionality from different aspects or domains are added in arbitrary nodes along this path. Eventually, the functionality of the subclasses is muddied with multiple responsibilities. Rather, given a composition model, each composed type can focus on its own purpose without compromising on flexibility. Further, it is easier to test each of these composed units rather than subclasses that are mixed in purpose.

TypeScript Guidelines

Use undefined instead of null

JavaScript has 2 bottom values: null and undefined. While they do have different semantic meanings, in most cases we recommend using undefined to represent the absence of a value. The exception to this rule is when a third party api, like the return value of React's render method expects null.

Use let or const instead of var

Historically, the only way to create a variable in JavaScript was using the var keyword. The var keyword is function scoped, which means the following code is valid JavaScript.

function foo() {
  if (true) {
    var x = 3;
  }
  console.log(x); // '3';
}

ES6 introduced let and const, which are block-scoped instead of function-scoped. The code below instead produces an error

function foo() {
  if (true) {
    let x = 3;
  }
  console.log(x); // 'x is not defined'
}

Prefer functions over classes

ES6 and TypeScript both introduce the class keyword which allows for developers to use more familiar OO paradigms. However, JavaScript supports functions as first class citizens, so you often don't need to use classes. Instead, you can often just create a module that exports a bunch of functions. We feel this encourages developers to favor composition over inheritance. State can still be stored as a variable inside a module if necessary. The class is also unavailable on ES5, so TypeScript will actually generate a fair bit of boiler plate code to provide support for classes on older browsers when compared to a module of functions.

The exception to this rule is React components, which we typically define using classes, however functional stateless components defined as functions are supported and should be used whenever the component doesn't need state or access to the lifecycle methods.

The main difference between classes and modules is that classes can be instantiated while modules cannot. As a general guideline if you are planning to have multiple instances of something with state associated with each instance and or planning to leverage inheritance, class should be favored. On the other hand, if you are grouping logically-connected sets of stateless functions together or just need a singleton, a module is more appropiate.

Use JavaScript plain objects instead of classes

It is often convenient to store state inside of a JavaScript object. Those coming from a C# background might be tempted to use a class for this purpose, but one can instead use JavaScript objects directly to achieve this purpose. In TypeScript, you can still achieve type safety by creating an interface or a type which describes the shape of your object. Note that TypeScript interfaces are very different from interfaces in C# since TypeScript uses "duck typing". An example of using plain objects can be seen below:

interface IPerson {
  name: string;
  age: number;
}

let myPerson: Person = { name: 'David', age: 70 };

Use arrow functions instead of bind

The this keyword in JavaScript has commonly been the source of many developer frustrations, particularly when trying to call functions within a class from a callback. To make sure that you are bound to the right this, developers have typically written code like this:

var onClick = this.callback.bind(this);
foo.addEventListener('click', onClick);

Starting with ES6, this can instead be achieved using arrow functions like this:

foo.addEventListener('click', () => this.callback());

For classes, you can also declare instance functions in TypeScript

class MyClass {
  private handleClick = e => {
    // code
  };
}

Avoid using any

Strive to provide type safety to your code by avoiding the use of the any keyword.

Use [] instead of Array

TypeScript has two mechanisms for declaring an array, which are shown below. We prefer the [] notation over the Array keyword since this aligns with the open source JavaScript community.

let list: number[] = [1, 2, 3]; // prefer this notation
let list: Array<number> = [1, 2, 3];

Use for or for of loops for highly treaded perforamcne sensitive code

While forEach loops can be more readable than traditional for loops, they add function / closure allocation and add stack entries, which especially in legacy browsers can add overhead. Using for of will transpile into a transitional for loop which is proven to be slightly faster. Performance issues are death by a thousand papercuts, so be aware to adopt best practices and in bulk it makes a difference.

Performance comparison of for vs forEach

For simple iteration use map, reduce, filter, and forEach instead of loops for simple cases

JavaScript has some great built in functions for common operations that happen in a loop, use them whenever possible.

Be wary of your browser matrix

IE11 does not support a number of features: [].find, WeakMap, some features in Map, IntersectionObserver, Promise and Object.assign are some of them. Be aware that these require polyfills if you use them, so avoid them or ensure your downstream use cases can polyfill them.

Most of these can fail type checking if you set your lib in tsconfig.json to ['dom'] only.

Use === and !== over == and !=

Use the type safe versions of the equality checks whenever possible. Remember, 8 == '8' is true, but 8 === '8' is false.

Use shortcuts for booleans, but explicit comparisons for strings and numbers

// bad
if (isValid === true) {
  // ...
}

// good
if (isValid) {
  // ...
}

// bad
if (name) {
  // ...
}

// good
if (name !== '') {
  // ...
}

// bad
if (collection.length) {
  // ...
}

// good
if (collection.length > 0) {
  // ...
}

Coding Style

If you are working with a project that isn't using Prettier, please take the time to enable it in your project so that these rules can be auto corrected.

Use 2 spaces instead of tabs for indentation

This aligns with the common practice in the open source JavaScript community.

Always surround conditionals and loops with curly braces

Bad

if (foo.length > 0) count += 1;

Good

if (foo.length > 0) {
  count += 1;
}

Open curly braces always go on the same line as the code that requires the brace

Bad

if (foo.length > 0)
{
  count += 1;
}

Good

if (foo.length > 0) {
  count += 1;
}

Use JSDoc style comments for function, interfaces, enums, and classes

Be sure to include comments that describes the functionality of the function, interface, enum, or class.

Quotes

Use single quotes for strings

With the exception of JSX attributes, use single quotes for strings.

Always use double quotes for JSX Attributes

Good

(Note that JSX attributes use double quotes to mimic HTML standards.)

<Foo bar="bar" />

Bad

<Foo bar='bar' />

Casing

Use camelCase when naming objects, functions, and instances.

Good

const thisIsMyObject = {};
function thisIsMyFunction() {}

Use PascalCase when naming classes, enums, or exported consts.

Bad

function user(options) {
  this.name = options.name;
}

export const bad = new user({
  name: 'nope'
});

Good

class User {
  constructor(options) {
    this.name = options.name;
  }
}

export const Good = new User({
  name: 'yup'
});

Use camelCase for exported functions. Your filename should be identical to your function's name

export function makeStyleGuide() {
  // ...
}

Naming

Use prefixed underscores to identify private consts, methods, and functions

const _privateConst = 1;

class Foo {
  private _member = 1;
}

function _privateFunction() {
  // ...
}

In modules with a single export, prefer exporting both a named and default export

Good

export function foo() {}
export default foo;

Bad

export function foo() {}

Prefix interfaces and types with I

TypeScript interfaces produce no code in the transpiled JavaScript output. This is an important distinction to understand and visualize, as we can alleviate bundle size increase concerns when they are used. Because of this, we prefix both interfaces and types with I.

Bad

interface Person {
  name: string;
  age: number;
}

type Thing = Person;

Good

interface IPerson {
  name: string;
  age: number;
}

type IThing = IPerson;

Avoid including the words Core, Utility, Common, or Helper in the names of interfaces, classes, functions, or modules

These words are generic and don't convey any meaning, use more descriptive words.

Acknowledgements

Many of these rules were inspired by the guidelines of Outlook Web, Typescript, Pillar Studio and AirBnb

Clone this wiki locally
You can’t perform that action at this time.