Chinese | English

Applying the concept of Clean Code to TypeScript is inspired by clean-code-javascript.

This article is translated from labs42io/clean-code-typescript.

directory

  1. Introduction to the
  2. variable
  3. function
  4. Objects and data structures
  5. class
  6. The principle of SOLID
  7. test
  8. concurrent
  9. Error handling
  10. formatting
  11. annotation

Introduction to the

This is not a TypeScript design specification. Rather, it applies Robert C. Martin’s software engineering book Clean Code to TypeScript to guide readers in writing easy-to-read, reusable, and reconfigurable software in TypeScript.

Not every principle is strictly adhered to, and even fewer are widely agreed upon. While this is just a guideline, it is a distillation of years of programming experience by Clean Code’s authors.

Software engineering has been around for more than 50 years, and we still have a lot to learn. When software architecture is as old as architecture itself, perhaps we have stricter rules to follow. Now, let these guidelines serve as a litmus test for evaluating the quality of your code and that of your team.

Also, understanding these principles won’t immediately make you a good programmer, nor will it mean years of working without making mistakes. Every piece of code starts from an imperfect point of view and continues to improve, just like clay becomes pottery. Enjoy the process!

variable

Variable names should make sense

Make meaningful distinctions to make it easier for readers to understand the meaning of variables.

Example:


function between<T> (a1: T, a2: T, a3: T) {

  return a2 <= a1 && a1 <= a3;

}

Copy the code

Is:


function between<T> (value: T, left: T, right: T) {

  return left <= value && value <= right;

}

Copy the code

Variable names can be spelled out

If you can’t read it, you’ll sound like an idiot talking about it.

Example:


class DtaRcrd102 {

  private genymdhms: Date;

  private modymdhms: Date;

  private pszqint = '102';

}

Copy the code

Is:


class Customer {

  private generationTimestamp: Date;

  private modificationTimestamp: Date;

  private recordId = '102';

}

Copy the code

Use uniform names for variables that have the same function

Example:


function getUserInfo() :User;

function getUserDetails() :User;

function getUserData() :User;

Copy the code

Is:


function getUser() :User;

Copy the code

Use searchable names

We read more code than we write, so readability and retrievability are very important. If you don’t extract and name variable names that make sense, you cheat the person reading the code. For code to be retrievable, TSLint helps identify unnamed constants.

Example:


// What the heck is 86400000 for?

setTimeout(restart, 86400000);

Copy the code

Is:


// Declare them as capitalized named constants.

const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;

setTimeout(restart, MILLISECONDS_IN_A_DAY);

Copy the code

Use self-explanatory variable names

Example:


declare const users:Map<string, User>;

for (const keyValue of users) {

  // iterate through users map

}

Copy the code

Is:


declare const users:Map<string, User>;

for (const [id, user] of users) {

  // iterate through users map

}

Copy the code

Avoid mental mapping

Don’t let people guess or imagine the meaning of variables; clarity is king.

Example:


const u = getUser();

const s = getSubscription();

const t = charge(u, s);

Copy the code

Is:


const user = getUser();

const subscription = getSubscription();

const transaction = charge(user, subscription);

Copy the code

Do not add useless context

If the class name or object name already expresses some information, do not repeat it in the internal variable name.

Example:


type Car = {

  carMake: string;

  carModel: string;

  carColor: string;

}

function print(car: Car) :void {

  console.log(`${this.carMake} ${this.carModel} (${this.carColor}) `);

}

Copy the code

Is:


type Car = {

  make: string;

  model: string;

  color: string;

}

function print(car: Car) :void {

  console.log(`${this.make} ${this.model} (${this.color}) `);

}

Copy the code

Use default parameters instead of short circuit or condition judgments

In general, the default parameters are neater than short-circuiting.

Example:


function loadPages(count: number) {

  constloadCount = count ! = =undefined ? count : 10;

  // ...

}

Copy the code

Is:


function loadPages(count: number = 10) {

  // ...

}

Copy the code

function

As few parameters as possible (ideally no more than 2)

Limit the number of arguments so that function testing is easier. More than three parameters can lead to a surge in test complexity, requiring many different parameter combinations to be tested. Ideally, only one or two parameters. If you have more than two arguments, your function may be too complex.

If you need a lot of parameters, consider using objects. To make the properties of functions clearer, destruct can be used, which has the following advantages:

  1. When someone looks at the function signature, it is immediately clear which attributes are used.

  2. Destructuring makes a deep copy of the argument object passed to the function, which prevents side effects. (Note: objects and arrays deconstructed from parameter objects are not cloned)

  3. TypeScript displays warnings for unused properties.

Example:


function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {

  // ...

}

createMenu('Foo'.'Bar'.'Baz'.true);

Copy the code

Is:


function createMenu(options: {title: string, body: string, buttonText: string, cancellable: boolean}) {

  // ...

}

createMenu({

  title: 'Foo',

  body: 'Bar',

  buttonText: 'Baz',

  cancellable: true

});

Copy the code

TypeScript type aliases further improve readability.


type MenuOptions = {title: string, body: string, buttonText: string, cancellable: boolean};

function createMenu(options: MenuOptions) {

  // ...

}

createMenu({

  title: 'Foo',

  body: 'Bar',

  buttonText: 'Baz',

  cancellable: true

});

Copy the code

Just do one thing

This is by far the most important rule in software engineering. If a function does more than one thing, it is harder to compose, test, and understand. Conversely, a function has only one behavior, which makes it easier to refactor and the code cleaner. If that’s all you know from this guide, you’re ahead of most programmers.

Example:


function emailClients(clients: Client) {

  clients.forEach((client) = > {

    const clientRecord = database.lookup(client);

    if(clientRecord.isActive()) { email(client); }}); }Copy the code

Is:


function emailClients(clients: Client) {

  clients.filter(isActiveClient).forEach(email);

}

function isActiveClient(client: Client) {

  const clientRecord = database.lookup(client);

  return clientRecord.isActive();

}

Copy the code

Worthy of the name

The function name shows what the function does.

Example:


function addToDate(date: Date, month: number) :Date {

  // ...

}

const date = new Date(a);// It's hard to tell from the function name what is added

addToDate(date, 1);

Copy the code

Is:


function addMonthToDate(date: Date, month: number) :Date {

  // ...

}

const date = new Date(a); addMonthToDate(date,1);

Copy the code

Each function contains only one level of abstraction

When there are multiple levels of abstraction, the function should be doing too much. Splitting up functions for reuse also makes testing easier.

Example:


function parseCode(code:string) {

  const REGEXES = [ / *... * / ];

  const statements = code.split(' ');

  const tokens = [];

  REGEXES.forEach((regex) = > {

    statements.forEach((statement) = > {

      // ...

    });

  });

  const ast = [];

  tokens.forEach((token) = > {

    // lex...

  });

  ast.forEach((node) = > {

    // parse...

  });

}

Copy the code

Is:


const REGEXES = [ / *... * / ];

function parseCode(code:string) {

  const tokens = tokenize(code);

  const syntaxTree = parse(tokens);

  syntaxTree.forEach((node) = > {

    // parse...

  });

}

function tokenize(code: string) :Token[] {

  const statements = code.split(' ');

  const tokens:Token[] = [];

  REGEXES.forEach((regex) = > {

    statements.forEach((statement) = > {

      tokens.push( / *... * / );

    });

  });

  return tokens;

}

function parse(tokens: Token[]) :SyntaxTree {

  const syntaxTree:SyntaxTree[] = [];

  tokens.forEach((token) = > {

    syntaxTree.push( / *... * / );

  });

  return syntaxTree;

}

Copy the code

Delete duplicate code

Repetition is the root of all evil! Repetition means that if you want to change one piece of logic, you need to change multiple pieces of code :cry:. Imagine running a restaurant and keeping track of your inventory: all the tomatoes, Onions, garlic, spices, etc. How painful it is to maintain multiple inventory lists!

Duplicate code exists because there are two or more functions that are very similar, with one difference, but that difference forces you to use multiple independent functions to do many of the same things. Removing duplicate code means creating an abstraction that can handle this different set of things with just one function/module/class.

Sound abstraction is crucial, which is why you should follow the SOLID principle. Bad abstractions can be worse than repetitive code, so be careful! Having said that, do a good job of abstraction! Try not to repeat.

Example:


function showDeveloperList(developers: Developer[]) {

  developers.forEach((developer) = > {

    const expectedSalary = developer.calculateExpectedSalary();

    const experience = developer.getExperience();

    const githubLink = developer.getGithubLink();

    const data = {

      expectedSalary,

      experience,

      githubLink

    };

    render(data);

  });

}

function showManagerList(managers: Manager[]) {

  managers.forEach((manager) = > {

    const expectedSalary = manager.calculateExpectedSalary();

    const experience = manager.getExperience();

    const portfolio = manager.getMBAProjects();

    const data = {

      expectedSalary,

      experience,

      portfolio

    };

    render(data);

  });

}

Copy the code

Is:


class Developer {

  // ...

  getExtraDetails() {

    return {

      githubLink: this.githubLink,

    }

  }

}

class Manager {

  // ...

  getExtraDetails() {

    return {

      portfolio: this.portfolio,

    }

  }

}

function showEmployeeList(employee: Developer | Manager) {

  employee.forEach((employee) = > {

    const expectedSalary = developer.calculateExpectedSalary();

    const experience = developer.getExperience();

    const extra = employee.getExtraDetails();

    const data = {

      expectedSalary,

      experience,

      extra,

    };

    render(data);

  });

}

Copy the code

Sometimes there is a trade-off between repeating code and introducing unnecessary abstractions that add complexity. When two different modules come from different domains and their implementations look similar, copying is acceptable and a little better than extracting common code. Because extracting common code leads to an indirect dependency between the two modules.

useObject.assignordeconstructionTo set the default object

Example:


typeMenuConfig = {title? :string, body? :string, buttonText? :string, cancellable? :boolean};

function createMenu(config: MenuConfig) {

  config.title = config.title || 'Foo';

  config.body = config.body || 'Bar';

  config.buttonText = config.buttonText || 'Baz'; config.cancellable = config.cancellable ! = =undefined ? config.cancellable : true;

}

const menuConfig = {

  title: null,

  body: 'Bar',

  buttonText: null,

  cancellable: true

};

createMenu(menuConfig);

Copy the code

Is:


typeMenuConfig = {title? :string, body? :string, buttonText? :string, cancellable? :boolean};

function createMenu(config: MenuConfig) {

  const menuConfig = Object.assign({

    title: 'Foo',

    body: 'Bar',

    buttonText: 'Baz',

    cancellable: true

  }, config);

}

createMenu({ body: 'Bar' });

Copy the code

Alternatively, you can use the default destruct:

type MenuConfig = {title? : string, body? : string, buttonText? : string, cancellable? : boolean}; function createMenu({title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true}: MenuConfig) { // ... } createMenu({ body: 'Bar' });Copy the code

To avoid side effects, it is not allowed to pass undefined or NULL explicitly. See the — strictnullCheck option in the TypeScript compiler.

Do not use Flag arguments

The Flag argument tells the user that the function does more than one thing. If the function uses booleans to implement different code logical paths, consider splitting them.

Example:


function createFile(name:string, temp:boolean) {

  if (temp) {

    fs.create(`./temp/${name}`);

  } else{ fs.create(name); }}Copy the code

Is:


function createFile(name:string) {

  fs.create(name);

}

function createTempFile(name:string) {

  fs.create(`./temp/${name}`);

}

Copy the code

Avoid side effects (part1)

A function is said to have side effects when it produces behavior other than “one input, one output.” Such as writing a file, changing global variables, or giving all your money to a stranger.

In some cases, the program requires some side effects. As in the previous example of writing files, you should keep these functions together and not modify a file with multiple functions/classes. Accomplish this requirement with one and only one Service.

The point is to avoid common pitfalls, such as sharing state between unstructured objects, using mutable data types, and not knowing where side effects occur. If you can do this, you may have the last laugh!

Example:


// Global variable referenced by following function.

// If we had another function that used this name, now it'd be an array and it could break it.

let name = 'Robert C. Martin';

function toBase64() {

  name = btoa(name);

}

toBase64(); // produces side effects to `name` variable

console.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='

Copy the code

Is:


// Global variable referenced by following function.

// If we had another function that used this name, now it'd be an array and it could break it.

const name = 'Robert C. Martin';

function toBase64(text:string) :string {

  return btoa(text);

}

const encodedName = toBase64(name);

console.log(name);

Copy the code

Avoid side effects (part2)

In JavaScript, primitive types are passed by value, and objects and arrays are passed by reference.

There is a case where if your function modifies the cart array to add purchased items, all other functions that use the CART array will be affected by the addition. Imagine a bad situation:

The user clicks the “Buy” button, which invokes the Purchase function, which requests the network and sends the CART array to the server. Because of the poor network connection, the purchase function must continually retry the request. What if a user accidentally clicks the “Add to Cart” button on an unwanted item just before the web request starts? When the network request starts, the Purchase function sends the unexpected addition because it references a shopping cart array that the addItemToCart function modifies to add unwanted items.

A good solution is for addItemToCart to always clone the Cart, edit it, and return the clone. This ensures that other functions that reference the shopping cart are not affected by any changes.

Two points to note:

  1. In some rare cases, you might actually want to modify the input object. And most can be reconstructed to ensure no side effects! (See pure function)

  2. In terms of performance, cloning large objects is really expensive. Fortunately, there are some good libraries that provide fast and efficient ways to do this without the memory footprint of manually cloning objects and arrays.

Example:


function addItemToCart(cart: CartItem[], item:Item) :void {

  cart.push({ item, date: Date.now() });

};

Copy the code

Is:


function addItemToCart(cart: CartItem[], item:Item) :CartItem[] {

  return [...cart, { item, date: Date.now() }];

};

Copy the code

Don’t write global functions

Polluting globally in JavaScript is very bad, and it can cause conflicts with other libraries that the user calling your API doesn’t know about until he or she gets an exception in the actual environment.

Consider this example: What if you wanted to extend a JavaScript Array to have a diff method that shows the difference between two arrays? You could write the new function to array.prototype, but it might conflict with another library that is trying to do the same thing. What if another library just uses diff to find the difference between the first and last element of an array?

A better approach is to extend Array to implement the corresponding functions.

Example:


declare global {

  interface Array<T> {

    diff(other: T[]): Array<T>; }}if (!Array.prototype.diff){

  Array.prototype.diff = function <T> (other: T[]) :T[] {

    const hash = new Set(other);

    return this.filter(elem= >! hash.has(elem)); }; }Copy the code

Is:


class MyArray<T> extends Array<T> {

  diff(other: T[]): T[] {

    const hash = new Set(other);

    return this.filter(elem= >! hash.has(elem)); }; }Copy the code

Functional programming is better than imperative programming

Use functional programming whenever possible!

Example:


const contributions = [

  {

    name: 'Uncle Bobby',

    linesOfCode: 500

  }, {

    name: 'Suzie Q',

    linesOfCode: 1500

  }, {

    name: 'Jimmy Gosling',

    linesOfCode: 150

  }, {

    name: 'Gracie Hopper',

    linesOfCode: 1000}];let totalOutput = 0;

for (let i = 0; i < contributions.length; i++) {

  totalOutput += contributions[i].linesOfCode;

}

Copy the code

Is:


const contributions = [

  {

    name: 'Uncle Bobby',

    linesOfCode: 500

  }, {

    name: 'Suzie Q',

    linesOfCode: 1500

  }, {

    name: 'Jimmy Gosling',

    linesOfCode: 150

  }, {

    name: 'Gracie Hopper',

    linesOfCode: 1000}];const totalOutput = contributions

  .reduce((totalLines, output) = > totalLines + output.linesOfCode, 0)

Copy the code

Package judgment condition

Example:


if (subscription.isTrial || account.balance > 0) {

  // ...

}

Copy the code

Is:


function canActivateService(subscription: Subscription, account: Account) {

  return subscription.isTrial || account.balance > 0

}

if (canActivateService(subscription, account)) {

  // ...

}

Copy the code

Avoid “negative” judgments

Example:


function isEmailNotUsed(email: string) {

  // ...

}

if (isEmailNotUsed(email)) {

  // ...

}

Copy the code

Is:


function isEmailUsed(email) {

  // ...

}

if(! isEmailUsed(node)) {// ...

}

Copy the code

Avoid judgment condition

It seems impossible to pull this off. The first thing most people think when they hear this is, “How can I function without an if statement?” In most cases, you can use polymorphism to achieve the same functionality. The next question is “Why?” The reason is the same as before: functions only do one thing.

Example:


class Airplane {

  private type: string;

  // ...

  getCruisingAltitude() {

    switch (this.type) {

      case '777':

        return this.getMaxAltitude() - this.getPassengerCount();

      case 'Air Force One':

        return this.getMaxAltitude();

      case 'Cessna':

        return this.getMaxAltitude() - this.getFuelExpenditure();

      default:

        throw new Error('Unknown airplane type.'); }}}Copy the code

Is:


class Airplane {

  // ...

}

class Boeing777 extends Airplane {

  // ...

  getCruisingAltitude() {

    return this.getMaxAltitude() - this.getPassengerCount(); }}class AirForceOne extends Airplane {

  // ...

  getCruisingAltitude() {

    return this.getMaxAltitude(); }}class Cessna extends Airplane {

  // ...

  getCruisingAltitude() {

    return this.getMaxAltitude() - this.getFuelExpenditure(); }}Copy the code

Avoiding type checking

TypeScript is a strict syntactic superset of JavaScript with static type checking features. So specifying the types of variables, parameters, and return values to take advantage of this feature makes refactoring easier.

Example:


function travelToTexas(vehicle: Bicycle | Car) {

  if (vehicle instanceof Bicycle) {

    vehicle.pedal(this.currentLocation, new Location('texas'));

  } else if (vehicle instanceof Car) {

    vehicle.drive(this.currentLocation, new Location('texas')); }}Copy the code

Is:


type Vehicle = Bicycle | Car;

function travelToTexas(vehicle: Vehicle) {

  vehicle.move(this.currentLocation, new Location('texas'));

}

Copy the code

Don’t over-optimize

Modern browsers do a lot of low-level optimizations at run time. A lot of times, you’re just wasting your time optimizing. There are some great resources to help locate where optimization is needed, find it, and fix it.

Example:


// On old browsers, each iteration with uncached `list.length` would be costly

// because of `list.length` recomputation. In modern browsers, this is optimized.

for (let i = 0, len = list.length; i < len; i++) {

  // ...

}

Copy the code

Is:


for (let i = 0; i < list.length; i++) {

  // ...

}

Copy the code

Delete useless code

Useless code and duplicate code need not be retained. If there is no place to call it, remove it! If you still need it, you can look at the version history.

Example:


function oldRequestModule(url: string) {

  // ...

}

function requestModule(url: string) {

  // ...

}

const req = requestModule;

inventoryTracker('apples', req, 'www.inventory-awesome.io');

Copy the code

Is:


function requestModule(url: string) {

  // ...

}

const req = requestModule;

inventoryTracker('apples', req, 'www.inventory-awesome.io');

Copy the code

Use iterators and generators

Use generators and iterators when working with collections of data as if they were streams.

The reasons are as follows:

  • The caller is decoupled from the generator, in the sense that the caller decides how many items to access.
  • Delay execution and use as needed.
  • Built-in supportfor-ofSyntax iteration
  • Allows the implementation of optimized iterator patterns

Example:

function fibonacci(n: number) :number[] {
  if (n === 1) return [0];
  if (n === 2) return [0.1];

  const items: number[] = [0.1];
  while (items.length < n) {
    items.push(items[items.length - 2] + items[items.length - 1]);
  }

  return items;
}

function print(n: number) {
  fibonacci(n).forEach(fib= > console.log(fib));
}

// Print first 10 Fibonacci numbers.
print(10);
Copy the code

Is:

// Generates an infinite stream of Fibonacci numbers.
// The generator doesn't keep the array of all numbers.
function* fibonacci() :IterableIterator<number> {
  let [a, b] = [0.1];

  while (true) {
    yielda; [a, b] = [b, a + b]; }}function print(n: number) {
  let i = 0;
  for (const fib in fibonacci()) {
    if (i++ === n) break;  
    console.log(fib); }}// Print first 10 Fibonacci numbers.
print(10);
Copy the code

Some libraries handle iterations in a similar way to native arrays by linking “map,” “slice,” “forEach,” and so on. See ITIRIRI for examples of advanced operations that use iterators (or itiRIri-async for asynchronous iterations).

import itiriri from 'itiriri';

function* fibonacci() :IterableIterator<number> {
  let [a, b] = [0.1];
 
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

itiriri(fibonacci())
  .take(10)
  .forEach(fib= > console.log(fib));
Copy the code

Objects and data structures

usegettersandsetters

TypeScript supports getter/setter syntax. Using getters and setters to access data from an object is better than simply looking up properties on the object. Here’s why:

  • You don’t have to look up and modify every accessor in your code when you need to do something before you get an object property.
  • performsetIt is easier to add validation when.
  • Encapsulate the internal representation.
  • Easier to add logging and error handling.
  • You can lazily load an object’s properties, such as fetching it from the server.

Example:


class BankAccount {

  balance: number = 0;

  // ...

}

const value = 100;

const account = new BankAccount();

if (value < 0) {

  throw new Error('Cannot set negative balance.');

}

account.balance = value;

Copy the code

Is:


class BankAccount {

  private accountBalance: number = 0;

  get balance(): number {

    return this.accountBalance;

  }

  set balance(value: number) {

    if (value < 0) {

      throw new Error('Cannot set negative balance.');

    }

    this.accountBalance = value;

  }

  // ...

}

const account = new BankAccount();

account.balance = 100;

Copy the code

Let objects have private/protected members

TypeScript class members support public(default), protected, and private access restrictions.

Example:


class Circle {

  radius: number;

  

  constructor(radius: number) {

    this.radius = radius;

  }

  perimeter(){

    return 2 * Math.PI * this.radius;

  }

  surface(){

    return Math.PI * this.radius * this.radius; }}Copy the code

Is:


class Circle {

  constructor(private readonly radius: number) {

  }

  perimeter(){

    return 2 * Math.PI * this.radius;

  }

  surface(){

    return Math.PI * this.radius * this.radius; }}Copy the code

invariance

The TypeScript type system allows you to make individual properties on interfaces and classes read-only and run as functions.

As an advanced scenario, you can use the built-in type Readonly, which accepts type T and marks all of its properties as read-only using the mapping type.

Example:


interface Config {

  host: string;

  port: string;

  db: string;

}

Copy the code

Is:


interface Config {

  readonly host: string;

  readonly port: string;

  readonly db: string;

}

Copy the code

Type vs Interface

Use types when you might want to join or intersect. If you need extensions or implementations, use interfaces. However, there are no hard and fast rules, only applicable rules.

See Typescript for an explanation of the difference between Type and interface.

Example:


interface EmailConfig {

  // ...

}

interface DbConfig {

  // ...

}

interface Config {

  // ...

}

/ /...

type Shape {

  // ...

}

Copy the code

Is:


type EmailConfig {

  // ...

}

type DbConfig {

  // ...

}

type Config  = EmailConfig | DbConfig;

// ...

interface Shape {

}

class Circle implements Shape {

  // ...

}

class Square implements Shape {

  // ...

}

Copy the code

class

Small, small, small! Tell the story three times

The size of a class is measured by its responsibilities. In accordance with the single responsibility principle, the class should be small.

Example:


class Dashboard {

  getLanguage(): string { / *... * / }

  setLanguage(language: string) :void { / *... * / }

  showProgress(): void { / *... * / }

  hideProgress(): void { / *... * / }

  isDirty(): boolean { / *... * / }

  disable(): void { / *... * / }

  enable(): void { / *... * / }

  addSubscription(subscription: Subscription): void { / *... * / }

  removeSubscription(subscription: Subscription): void { / *... * / }

  addUser(user: User): void { / *... * / }

  removeUser(user: User): void { / *... * / }

  goToHomePage(): void { / *... * / }

  updateProfile(details: UserDetails): void { / *... * / }

  getVersion(): string { / *... * / }

  // ...

}

Copy the code

Is:


class Dashboard {

  disable(): void { / *... * / }

  enable(): void { / *... * / }

  getVersion(): string { / *... * /}}// split the responsibilities by moving the remaining methods to other classes

// ...

Copy the code

High cohesion and low coupling

Cohesion: Defines the degree to which members of a class are related to each other. Ideally, every method with high cohesion should use all the fields in the class, which is neither possible nor desirable. But we still advocate high cohesion.

Coupling: Refers to the degree to which two classes are related. If a change in one class does not affect the other, it is called a low-coupling class.

Good software design has high cohesion and low coupling.

Example:


class UserManager {

  // Bad: each private variable is used by one or another group of methods.

  // It makes clear evidence that the class is holding more than a single responsibility.

  // If I need only to create the service to get the transactions for a user,

  // I'm still forced to pass and instance of emailSender.

  constructor(

    private readonly db: Database,

    private readonly emailSender: EmailSender) {}async getUser(id: number) :Promise<User> {

    return await db.users.findOne({ id })

  }

  async getTransactions(userId: number) :Promise<Transaction[]> {

    return await db.transactions.find({ userId })

  }

  async sendGreeting(): Promise<void> {

    await emailSender.send('Welcome! ');

  }

  async sendNotification(text: string) :Promise<void> {

    await emailSender.send(text);

  }

  async sendNewsletter(): Promise<void> {

    // ...}}Copy the code

Is:


class UserService {

  constructor(private readonly db: Database) {}async getUser(id: number) :Promise<User> {

    return await db.users.findOne({ id })

  }

  async getTransactions(userId: number) :Promise<Transaction[]> {

    return await db.transactions.find({ userId })

  }

}

class UserNotifier {

  constructor(private readonly emailSender: EmailSender) {}async sendGreeting(): Promise<void> {

    await emailSender.send('Welcome! ');

  }

  async sendNotification(text: string) :Promise<void> {

    await emailSender.send(text);

  }

  async sendNewsletter(): Promise<void> {

    // ...}}Copy the code

Composition over Inheritance

As the Gang of Four points out in design patterns, you use composition rather than inheritance whenever possible. Both composition and inheritance have their advantages and disadvantages. The main point of this maxim is that if you are subconsciously inclined towards inheritance, try to think about whether combinations can better model your problem, and in some cases they can.

When should inheritance be used? It depends on the problem you are facing. Inheritance is better for the following scenarios:

  1. Inheritance represents an “IS-A” relationship, not a “has-A” relationship (people -> animals vs. User -> User Details).
  2. Code for reusable base classes (humans can move like all animals).
  3. You want to make global changes to the derived classes by changing the base class (changing the caloric expenditure of all animals while moving).

Example:


class Employee {

  constructor(

    private readonly name: string.private readonly email:string) {}// ...

}

// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee

class EmployeeTaxData extends Employee {

  constructor(

    name: string, 

    email:string.private readonly ssn: string.private readonly salary: number) {

    super(name, email);

  }

  // ...

}

Copy the code

Is:


class Employee {

  private taxData: EmployeeTaxData;

  constructor(

    private readonly name: string.private readonly email:string) {

  }

  setTaxData(ssn: string, salary: number): Employee {

    this.taxData = new EmployeeTaxData(ssn, salary);

    return this;

  }

  // ...

}

class EmployeeTaxData {

  constructor(

    public readonly ssn: string.public readonly salary: number) {}// ...

}

Copy the code

Method chain

Very useful pattern, found in many libraries. It makes code more expressive and concise.

Example:


class QueryBuilder {

  private collection: string;

  private pageNumber: number = 1;

  private itemsPerPage: number = 100;

  private orderByFields: string[] = [];

  from(collection: string) :void {

    this.collection = collection;

  }

  page(number: number, itemsPerPage: number = 100) :void {

    this.pageNumber = number;

    this.itemsPerPage = itemsPerPage; } orderBy(... fields:string[]) :void {

    this.orderByFields = fields;

  }

  build(): Query {

    // ...}}// ...

const query = new QueryBuilder();

query.from('users');

query.page(1.100);

query.orderBy('firstName'.'lastName');

const query = queryBuilder.build();

Copy the code

Is:


class QueryBuilder {

  private collection: string;

  private pageNumber: number = 1;

  private itemsPerPage: number = 100;

  private orderByFields: string[] = [];

  from(collection: string) :this {

    this.collection = collection;

    return this;

  }

  page(number: number, itemsPerPage: number = 100) :this {

    this.pageNumber = number;

    this.itemsPerPage = itemsPerPage;

    return this; } orderBy(... fields:string[]) :this {

    this.orderByFields = fields;

    return this;

  }

  build(): Query {

    // ...}}// ...

const query = new QueryBuilder()

  .from('users')

  .page(1.100)

  .orderBy('firstName'.'lastName')

  .build();

Copy the code

The principle of SOLID

Single Responsibility Principle (SRP)

As Clean Code states, “There should be no more than one reason for a class to change.” It seems tempting to pack a lot of functionality into a single class, like you can only bring one suitcase on a flight. The problem with this is that classes are conceptually not cohesive, and there are many reasons to modify classes. We should minimize the number of class changes. If a class has too many features, modifying one of them can be difficult to determine the impact on other dependent modules in the code base.

Example:


class UserSettings {

  constructor(private readonly user: User) {

  }

  changeSettings(settings: UserSettings) {

    if (this.verifyCredentials()) {

      // ...

    }

  }

  verifyCredentials() {

    // ...}}Copy the code

Is:


class UserAuth {

  constructor(private readonly user: User) {

  }

  verifyCredentials() {

    // ...}}class UserSettings {

  private readonly auth: UserAuth;

  constructor(private readonly user: User) {

    this.auth = new UserAuth(user);

  }

  changeSettings(settings: UserSettings) {

    if (this.auth.verifyCredentials()) {

      // ...}}}Copy the code

Open close Principle (OCP)

As Bertrand Meyer says, “Software entities (classes, modules, functions, etc.) should be open to extension and closed to modification.” In other words, it allows you to add new functionality without changing existing code.

Example:


class AjaxAdapter extends Adapter {

  constructor() {

    super(a); }// ...

}

class NodeAdapter extends Adapter {

  constructor() {

    super(a); }// ...

}

class HttpRequester {

  constructor(private readonly adapter: Adapter) {}async fetch<T>(url: string) :Promise<T> {

    if (this.adapter instanceof AjaxAdapter) {

      const response = await makeAjaxCall<T>(url);

      // transform response and return

    } else if (this.adapter instanceof NodeAdapter) {

      const response = await makeHttpCall<T>(url);

      // transform response and return}}}function makeAjaxCall<T> (url: string) :Promise<T> {

  // request and return promise

}

function makeHttpCall<T> (url: string) :Promise<T> {

  // request and return promise

}

Copy the code

Is:


abstract class Adapter {

  abstract async request<T>(url: string) :Promise<T>;

}

class AjaxAdapter extends Adapter {

  constructor() {

    super(a); }async request<T>(url: string) :Promise<T>{

    // request and return promise

  }

  // ...

}

class NodeAdapter extends Adapter {

  constructor() {

    super(a); }async request<T>(url: string) :Promise<T>{

    // request and return promise

  }

  // ...

}

class HttpRequester {

  constructor(private readonly adapter: Adapter) {}async fetch<T>(url: string) :Promise<T> {

    const response = await this.adapter.request<T>(url);

    // transform response and return}}Copy the code

Richter’s Substitution Principle (LSP)

It’s a terrible term for a very simple concept.

Its formal definition is: “If S is a subtype of T, then an object of type T can be replaced with an object of type S without changing any desired properties of the program (correctness, task performed, etc.).” This is an even scarier definition.

A better explanation is that if you have a parent and a subclass, the parent and subclass can be used interchangeably without problems. This can still be confusing, so let’s take a look at the classic square rectangle example. Mathematically, a square is a rectangle, but if you model it using an “IS-A” relationship through inheritance, you quickly run into trouble.

Example:


class Rectangle {

  constructor(

    protected width: number = 0, 

    protected height: number = 0) {

  }

  setColor(color: string) {

    // ...

  }

  render(area: number) {

    // ...

  }

  setWidth(width: number) {

    this.width = width;

  }

  setHeight(height: number) {

    this.height = height;

  }

  getArea(): number {

    return this.width * this.height; }}class Square extends Rectangle {

  setWidth(width: number) {

    this.width = width;

    this.height = width;

  }

  setHeight(height: number) {

    this.width = height;

    this.height = height; }}function renderLargeRectangles(rectangles: Rectangle[]) {

  rectangles.forEach((rectangle) = > {

    rectangle.setWidth(4);

    rectangle.setHeight(5);

    const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.

    rectangle.render(area);

  });

}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];

renderLargeRectangles(rectangles);

Copy the code

Is:


abstract class Shape {

  setColor(color: string) {

    // ...

  }

  render(area: number) {

    // ...

  }

  abstract getArea(): number;

}

class Rectangle extends Shape {

  constructor(

    private readonly width = 0, 

    private readonly height = 0) {

    super(a); } getArea():number {

    return this.width * this.height; }}class Square extends Shape {

  constructor(private readonly length: number) {

    super(a); } getArea():number {

    return this.length * this.length; }}function renderLargeShapes(shapes: Shape[]) {

  shapes.forEach((shape) = > {

    const area = shape.getArea();

    shape.render(area);

  });

}

const shapes = [new Rectangle(4.5), new Rectangle(4.5), new Square(5)];

renderLargeShapes(shapes);

Copy the code

Interface Isolation Principle (ISP)

“Customers should not be forced to rely on interfaces they don’t use.” This principle is closely related to the principle of single liability. This means that you should not design a large, all-encompassing abstraction, which would add to the customer’s burden of implementing methods they don’t need.

Example:


interface ISmartPrinter {

  print();

  fax();

  scan();

}

class AllInOnePrinter implements ISmartPrinter {

  print() {

    // ...

  }  

  

  fax() {

    // ...

  }

  scan() {

    // ...}}class EconomicPrinter implements ISmartPrinter {

  print() {

    // ...

  }  

  

  fax() {

    throw new Error('Fax not supported.');

  }

  scan() {

    throw new Error('Scan not supported.'); }}Copy the code

Is:


interface IPrinter {

  print();

}

interface IFax {

  fax();

}

interface IScanner {

  scan();

}

class AllInOnePrinter implements IPrinter, IFax, IScanner {

  print() {

    // ...

  }  

  

  fax() {

    // ...

  }

  scan() {

    // ...}}class EconomicPrinter implements IPrinter {

  print() {

    // ...}}Copy the code

Dependency Inversion Principle

This principle has two main points:

  1. High-level modules should not depend on low-level modules; both should depend on abstractions.
  2. Abstraction does not depend on implementation; implementation should depend on abstraction.

This is hard to understand at first, but if you use Angular, you’ll see that this principle is implemented in the form of dependency injection (DI). Although the concept is different, DIP prevents a high-level module from knowing the details of its low-level modules and setting them up. It can do this through DI. A huge benefit of doing this is to reduce coupling between modules. Coupling is bad, and it makes code hard to refactor.

Dips are typically implemented using inversion of control (IoC) containers. For example: TypeScript’s IoC container, InversifyJs

Example:


import { readFile as readFileCb } from 'fs';

import { promisify } from 'util';

const readFile = promisify(readFileCb);

type ReportData = {

  // ..

}

class XmlFormatter {

  parse<T>(content: string): T {

    // Converts an XML string to an object T}}class ReportReader {

  // BAD: We have created a dependency on a specific request implementation.

  // We should just have ReportReader depend on a parse method: `parse`

  private readonly formatter = new XmlFormatter();

  async read(path: string) :Promise<ReportData> {

    const text = await readFile(path, 'UTF8');

    return this.formatter.parse<ReportData>(text); }}// ...

const reader = new ReportReader();

await report = await reader.read('report.xml');

Copy the code

Is:


import { readFile as readFileCb } from 'fs';

import { promisify } from 'util';

const readFile = promisify(readFileCb);

type ReportData = {

  // ..

}

interface Formatter {

  parse<T>(content: string): T;

}

class XmlFormatter implements Formatter {

  parse<T>(content: string): T {

    // Converts an XML string to an object T}}class JsonFormatter implements Formatter {

  parse<T>(content: string): T {

    // Converts a JSON string to an object T}}class ReportReader {

  constructor(private readonly formatter: Formatter){

  }

  async read(path: string) :Promise<ReportData> {

    const text = await readFile(path, 'UTF8');

    return this.formatter.parse<ReportData>(text); }}// ...

const reader = new ReportReader(new XmlFormatter());

await report = await reader.read('report.xml');

// or if we had to read a json report:

const reader = new ReportReader(new JsonFormatter());

await report = await reader.read('report.json');

Copy the code

test

Testing is more important than shipping. If there are no tests or insufficient numbers, there is no guarantee that you won’t introduce problems every time you release code. What is enough testing? It’s up to the team, but having 100% coverage (all statements and branches) gives the team more confidence. All of this is based on a good testing framework and coverage tools.

There is no reason not to write tests. There are many excellent JS testing frameworks that support TypeScript. Find one that your team likes. Then write tests for each new feature/module. Great if you like test-driven development (TDD), where the emphasis is on ensuring that code coverage is met before developing any features or refactoring existing features.

The three Laws of TDD

  1. Do not write production code until you write unit tests that fail.
  2. You can only write unit tests that just fail, not compile.
  3. You can only write production code that is just enough to pass the current failing test.

F.I.R.S.T. guidelines

Clean testing should follow the following guidelines:

  • Fast, testing should be Fast (feedback on business code problems in a timely manner).
  • Independent, each test process should be Independent.
  • Repeatable, the test should be Repeatable in any environment.
  • Self validationTesting results should be specific because you are pregnantthroughorfailure.
  • Timely, the test code should be written before the production code.

Single test each concept

Tests should also follow the single responsibility principle, making only one assertion per unit test.

Example:


import { assert } from 'chai';

describe('AwesomeDate'.(a)= > {

  it('handles date boundaries'.(a)= > {

    let date: AwesomeDate;

    date = new AwesomeDate('1/1/2015');

    date.addDays(30);

    assert.equal('1/31/2015', date);

    date = new AwesomeDate('2/1/2016');

    date.addDays(28);

    assert.equal('02/29/2016', date);

    date = new AwesomeDate('2/1/2015');

    date.addDays(28);

    assert.equal('03/01/2015', date);

  });

});

Copy the code

Is:


import { assert } from 'chai';

describe('AwesomeDate'.(a)= > {

  it('handles 30-day months'.(a)= > {

    const date = new AwesomeDate('1/1/2015');

    date.addDays(30);

    assert.equal('1/31/2015', date);

  });

  it('handles leap year'.(a)= > {

    const date = new AwesomeDate('2/1/2016');

    date.addDays(28);

    assert.equal('02/29/2016', date);

  });

  it('handles non-leap year'.(a)= > {

    const date = new AwesomeDate('2/1/2015');

    date.addDays(28);

    assert.equal('03/01/2015', date);

  });

});

Copy the code

The test case name should show its intent

When a test fails, the first sign that something is wrong may be its name.

Example:


describe('Calendar'.(a)= > {

  it('2/29/2020'.(a)= > {

    // ...

  });

  it('throws'.(a)= > {

    // ...

  });

});

Copy the code

Is:


describe('Calendar'.(a)= > {

  it('should handle leap year'.(a)= > {

    // ...

  });

  it('should throw when format is invalid'.(a)= > {

    // ...

  });

});

Copy the code

concurrent

Promises instead of calls back

Callbacks are not neat and lead to excessive nesting *(callback hell)*.

Some tools use callbacks to convert existing functions into promise objects:

  • Node. Js see util. Promisify
  • See pify, ES6-promisify for general use

Example:

import { get } from 'request'; import { writeFile } from 'fs'; function downloadPage(url: string, saveTo: string, callback: (error: Error, content? : string) => void){ get(url, (error, response) => { if (error) { callback(error); } else { writeFile(saveTo, response.body, (error) => { if (error) { callback(error); } else { callback(null, response.body); }}); } }) } downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => { if (error) { console.error(error); } else { console.log(content); }});Copy the code

Is:


import { get } from 'request';

import { writeFile } from 'fs';

import { promisify } from 'util';

const write = promisify(writeFile);

function downloadPage(url: string, saveTo: string) :Promise<string> {

  return get(url)

    .then(response= > write(saveTo, response))

}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'.'article.html')

  .then(content= > console.log(content))

  .catch(error= > console.error(error));  

Copy the code

Promise provides some helper methods to make the code more concise:

methods describe
Promise.resolve(value) Return a promise parsed from the incoming value.
Promise.reject(error) Return a promise with a reason for the rejection.
Promise.all(promises) Returns a new Promise, passed into the arrayeachA promise is not completed until it is returned, or the first promise is rejected.
Promise.race(promises) Returns a new Promise, passed into the arrayaA promise resolves or rejects, and the returned promise resolves or rejects.

Promise.all is especially useful when running tasks in parallel, and promise.race makes it easier to implement timeouts for promises.

Async/AwaitPromisesbetter

With the async/await syntax, you can write cleaner, more understandable chained promise code. A function prefixes the async keyword, and JavaScript runtime suspends code execution on the await keyword (when using promises).

Example:


import { get } from 'request';

import { writeFile } from 'fs';

import { promisify } from 'util';

const write = util.promisify(writeFile);

function downloadPage(url: string, saveTo: string) :Promise<string> {

  return get(url).then(response= > write(saveTo, response))

}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'.'article.html')

  .then(content= > console.log(content))

  .catch(error= > console.error(error));  

Copy the code

Is:


import { get } from 'request';

import { writeFile } from 'fs';

import { promisify } from 'util';

const write = promisify(writeFile);

async function downloadPage(url: string, saveTo: string) :Promise<string> {

  const response = await get(url);

  await write(saveTo, response);

  return response;

}

// somewhere in an async function

try {

  const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'.'article.html');

  console.log(content);

} catch (error) {

  console.error(error);

}

Copy the code

Error handling

Throwing errors is a good thing! It indicates that the runtime has successfully identified errors in the program, letting you know by stopping function execution on the current stack, terminating the process (in Node.js), and printing stack information in the console.

throwErrorOr usereject

JavaScript and TypeScript allow you to throw any object. Promises can also be rejected for any reason.

Throw syntax of type Error is recommended. Because your errors can be caught in high-level code with catch syntax. Capturing string messages there is messy and makes debugging more painful. For the same reason, you should use the Error type when rejecting promises.

Example:


function calculateTotal(items: Item[]) :number {

  throw 'Not implemented.';

}

function get() :Promise<Item[] >{

  return Promise.reject('Not implemented.');

}

Copy the code

Is:


function calculateTotal(items: Item[]) :number {

  throw new Error('Not implemented.');

}

function get() :Promise<Item[] >{

  return Promise.reject(new Error('Not implemented.'));

}

// or equivalent to:

async function get() :Promise<Item[] >{

  throw new Error('Not implemented.');

}

Copy the code

The advantage of using the Error type is that the try/catch/finally syntax supports it, and implicitly all errors have a stack property, which is useful for debugging.

Also, TypeScript is easier in this area, even if it doesn’t throw syntax and returns custom error objects instead. Consider the following example:


type Failable<R, E> = {

  isError: true;

  error: E;

} | {

  isError: false;

  value: R;

}

function calculateTotal(items: Item[]) :Failable<number, 'empty'> {

  if (items.length === 0) {

    return { isError: true, error: 'empty' };

  }

  // ...

  return { isError: false, value: 42 };

}

Copy the code

Please refer to the original text for detailed explanation.

Don’t forget to catch errors

Catching errors without handling them is actually not fixing them, and logging errors to the console (console.log) is not much better, as it is often lost in the console’s massive logs. If you write your code in a try/catch, that’s where an error is likely to occur, so you should consider doing something when an error occurs.

Example:


try {

  functionThatMightThrow();

} catch (error) {

  console.log(error);

}

// or even worse

try {

  functionThatMightThrow();

} catch (error) {

  // ignore error

}

Copy the code

Is:


import { logger } from './logging'

try {

  functionThatMightThrow();

} catch (error) {

  logger.log(error);

}

Copy the code

Don’t ignore promises that are rejected

For the same reason that an Error cannot be ignored in a try/catch.

Example:


getUser()

  .then((user: User) = > {

    return sendEmail(user.email, 'Welcome! ');

  })

  .catch((error) = > {

    console.log(error);

  });

Copy the code

Is:


import { logger } from './logging'

getUser()

  .then((user: User) = > {

    return sendEmail(user.email, 'Welcome! ');

  })

  .catch((error) = > {

    logger.log(error);

  });

// or using the async/await syntax:

try {

  const user = await getUser();

  await sendEmail(user.email, 'Welcome! ');

} catch (error) {

  logger.log(error);

}

Copy the code

formatting

Like many of the rules here, nothing is hard and fast, nor is formatting. The point is not to argue about formatting, but to use automated tools for formatting. Debating formats is a waste of time and money for engineers. The general rule is to maintain consistent formatting rules.

There is a powerful tool for TypeScript called TSLint. It is a static analysis tool that helps you dramatically improve the readability and maintainability of your code. For use in the project, you can refer to the following TSLint configuration:

  • TSLint Config Standard – Standard format rules

  • TSLint Config Airbnb – Airbnb format rules

  • TSLint Clean Code – Inspired by Clean Code: A Handbook of Agile Software Craftsmanship.

  • TSLint react – React related Lint rules

  • TSLint + Prettier – Prettier code formatting the related Lint rule

  • ESLint rules for tslint-typescript ESLint

  • Immutable – Disables mutation rules in TypeScript

You can also refer to the source code for the TypeScript style guide and coding conventions.

Case consistency

Capitals can tell you a lot about variables, functions, and so on. These are subjective rules, and your team makes the choice. The key is to be consistent no matter what you choose.

Example:


const DAYS_IN_WEEK = 7;

const daysInMonth = 30;

const songs = ['Back In Black'.'Stairway to Heaven'.'Hey Jude'];

const Artists = ['ACDC'.'Led Zeppelin'.'The Beatles'];

function eraseDatabase() {}

function restore_database() {}

class animal {}

class Container {}

Copy the code

Is:


const DAYS_IN_WEEK = 7;

const DAYS_IN_MONTH = 30;

const SONGS = ['Back In Black'.'Stairway to Heaven'.'Hey Jude'];

const ARTISTS = ['ACDC'.'Led Zeppelin'.'The Beatles'];

function eraseDatabase() {}

function restoreDatabase() {}

class Animal {}

class Container {}

Copy the code

It is best to use PASCAL names for class, interface, type, and namespace names.

Variables, functions, and class members use “hump naming.”

The calling function and the called function should be placed next to each other

When functions call each other, they should be placed close together. It is best to write the caller above the caller. It’s like reading a newspaper. We all read from the top down, so does reading code.

Example:


class PerformanceReview {

  constructor(private readonly employee: Employee) {}private lookupPeers() {

    return db.lookup(this.employee.id, 'peers');

  }

  private lookupManager() {

    return db.lookup(this.employee, 'manager');

  }

  private getPeerReviews() {

    const peers = this.lookupPeers();

    // ...

  }

  review() {

    this.getPeerReviews();

    this.getManagerReview();

    this.getSelfReview();

    // ...

  }

  private getManagerReview() {

    const manager = this.lookupManager();

  }

  private getSelfReview() {

    // ...}}const review = new PerformanceReview(employee);

review.review();

Copy the code

Is:


class PerformanceReview {

  constructor(private readonly employee: Employee) {

  }

  review() {

    this.getPeerReviews();

    this.getManagerReview();

    this.getSelfReview();

    // ...

  }

  private getPeerReviews() {

    const peers = this.lookupPeers();

    // ...

  }

  private lookupPeers() {

    return db.lookup(this.employee.id, 'peers');

  }

  private getManagerReview() {

    const manager = this.lookupManager();

  }

  private lookupManager() {

    return db.lookup(this.employee, 'manager');

  } 

  private getSelfReview() {

    // ...}}const review = new PerformanceReview(employee);

review.review();

Copy the code

Organize imports

Using clean and easy-to-read import statements, you can quickly see the dependencies of the current code. Import statements should follow the following:

  • ImportStatements should be arranged alphabetically and grouped.
  • Unused import statements should be deleted.
  • Named imports must be in alphabetical order (for example:import {A, B, C} from 'foo';).
  • Import sources must be arranged alphabetically in groups. Such as:import * as foo from 'a'; import * as bar from 'b';
  • Import groups are separated by blank lines.
  • The group is sorted as follows:
    • Polyfills (e.g.import 'reflect-metadata';)
    • Node built-in modules (for example:import fs from 'fs';)
    • External modules (e.g.import { query } from 'itiriri';)
    • Internal modules (e.g.import { UserService } from 'src/services/userService';)
    • Modules in the parent directory (e.g.import foo from '.. /foo'; import qux from '.. /.. /foo/qux';)
    • Modules from the same or sibling directory (e.g.import bar from './bar'; import baz from './bar/baz';)

Example:

import { TypeDefinition } from '.. /types/typeDefinition';
import { AttributeTypes } from '.. /model/attribute';
import { ApiCredentials, Adapters } from './common/api/authorization';
import fs from 'fs';
import { ConfigPlugin } from './plugins/config/configPlugin';
import { BindingScopeEnum, Container } from 'inversify';
import 'reflect-metadata';
Copy the code

Is:

import 'reflect-metadata';

import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';

import { AttributeTypes } from '.. /model/attribute';
import { TypeDefinition } from '.. /types/typeDefinition';

import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';
Copy the code

Use typescript aliases

To create neat import statements, you can set the paths and baseUrl properties of the compiler options in tsconfig.json.

This avoids using long relative paths when importing.

Example:

import { UserService } from '.. /.. /.. /services/UserService';
Copy the code

Is:

import { UserService } from '@services/UserService';
Copy the code
// tsconfig.json."compilerOptions": {..."baseUrl": "src"."paths": {
      "@services": ["services/*"]}... }...Copy the code

annotation

Writing comments means you can’t express yourself without comments, and it’s best to express yourself in code.

Don’t comment out bad code, rewrite it! — Brian W. Kernighan and P. J. Plaugher

The code interprets itself rather than using comments

Code is documentation.

Example:


// Check if subscription is active.

if (subscription.endDate > Date.now) {  }

Copy the code

Is:


const isSubscriptionActive = subscription.endDate > Date.now;

if (isSubscriptionActive) { / *... * / }

Copy the code

Do not leave commented out code in the code base

One reason for versioning is to make old code history.

Example:


class User {

  name: string;

  email: string;

  // age: number;

  // jobPosition: string;

}

Copy the code

Is:


class User {

  name: string;

  email: string;

}

Copy the code

Don’t write notes like a journal

Remember, use version control! There is no need to keep useless code, commented out code, especially comments like diaries. Use git log to get the history.

Example:


/** * 2016-12-20: Removed monads, didn't understand them (RM) * 2016-10-01: Improved using special monads (JP) * 2016-02-03: Added type-checking (LI) * 2015-03-14: Implemented combine (JR) */

function combine(a:number, b:number) :number {

  return a + b;

}

Copy the code

Is:


function combine(a:number, b:number) :number {

  return a + b;

}

Copy the code

Avoid using comments to mark locations

They often mess with code. To keep code structured, functions and variables should be properly indented and formatted.

Alternatively, you can use an IDE that supports Code folding (see Visual Studio Code Code folding).

Example:


////////////////////////////////////////////////////////////////////////////////

// Client class

////////////////////////////////////////////////////////////////////////////////

class Client {

  id: number;

  name: string;

  address: Address;

  contact: Contact;

  ////////////////////////////////////////////////////////////////////////////////

  // public methods

  ////////////////////////////////////////////////////////////////////////////////

  public describe(): string {

    // ...

  }

  ////////////////////////////////////////////////////////////////////////////////

  // private methods

  ////////////////////////////////////////////////////////////////////////////////

  private describeAddress(): string {

    // ...

  }

  private describeContact(): string {

    // ...}};Copy the code

Is:


class Client {

  id: number;

  name: string;

  address: Address;

  contact: Contact;

  public describe(): string {

    // ...

  }

  private describeAddress(): string {

    // ...

  }

  private describeContact(): string {

    // ...}};Copy the code

TODO comment

Use the // TODO annotation when you find yourself needing to leave a comment in your code to remind you of future improvements. Most ides provide special support for this type of annotation, and you can quickly browse through the entire TODO list.

However, remember that TODO annotations are not an excuse for bad code.

Example:

function getActiveSubscriptions() :Promise<Subscription[] >{
  // ensure `dueDate` is indexed.
  return db.subscriptions.find({ dueDate: { $lte: new Date()}}); }Copy the code

Is:

function getActiveSubscriptions() :Promise<Subscription[] >{
  // TODO: ensure `dueDate` is indexed.
  return db.subscriptions.find({ dueDate: { $lte: new Date()}}); }Copy the code