Clean code:

TypeScript version

Lukas Gamper, uSystems GmbH

https://labs42io.github.io/clean-code-typescript

Outline

  • Callback vs Promise
  • Error Handing
  • Functional vs imperative
  • Optimisations
  • SOLID

Callback vs Promise

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('..url..', '..fn..');
  console.log(content);
} catch (error) {
  console.error(error);
}
import { get } from 'request';
import { writeFile } from 'fs';

function downloadPage(
  url: string, 
  name: string, 
  cb: (err: Error, content?: string) => void
) {
  get(url, (err, response) => {
    if (err) {
      callback(error);
    } else {
      writeFile(name, response.body, (err) => {
        if (err) {
          cb(err);
        } else {
          cb(null, response.body);
        }
      });
    }
  });
}
downloadPage('..url..', '..fn..', (err, data) => {
  if (err) {
    console.error(error);
  } else {
    console.log(data);
  }
});

Bad

Good

Outline

  • Callback vs Promise
  • Error Handing
  • Functional vs imperative
  • Optimisations
  • SOLID

Never throw primitives

function calculateTotal(items: Item[]): number {
  throw new Error('Not implemented.');
}

function get(): Promise<Item[]> {
  return Promise.reject(new Error('Not implemented.'));
}

// or better:

async function get(): Promise<Item[]> {
  throw new Error('Not implemented.');
}
function calculateTotal(items: Item[]): number {
  throw 'Not implemented.';
}

function get(): Promise<Item[]> {
  return Promise.reject('Not implemented.');
}

Bad

Good

Don’t ignore caught errors

import { logger } from './logging'

try {
  function ThatMightThrow();
} catch (error) {
  logger.log(error);
}

try {
  const user = await getUser();
  await sendEmail(user.email, 'Welcome!');
} catch (error) {
  logger.log(error);
}
try {
  function ThatMightThrow();
} catch (error) {
  console.log(error);
}

getUser()
  .then((user: User) => { ... })
  .catch((error) => {
    console.log(error);
  });

Bad

Good

Outline

  • Callback vs Promise
  • Error Handing
  • Functional vs imperative
  • Optimisations
  • SOLID

Favor functional over imperative

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);
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;
}

Bad

Good

Outline

  • Callback vs Promise
  • Error Handing
  • Functional vs imperative
  • Optimisations
  • SOLID

Optimisation

for (let i = 0; i < list.length; i++) {
  // ...
}
// 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++) {
  // ...
}

Bad

Good

  1. Do not optimise.
  2. Do not optimise without measuring.

Outline

  • Callback vs Promise
  • Error Handing
  • Functional vs imperative
  • Optimisations
  • SOLID

SOLID

  • Single responsibility principle
  • Open–closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

Single Responsibility Principle

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()) {
      // ...
    }
  }
}
class UserSettings {
  constructor(
    private readonly user: User
  ) {}

  changeSettings(settings: UserSettings) {
    if (this.verifyCredentials()) {
      // ...
    }
  }

  verifyCredentials() {
    // ...
  }
}

Bad

Good

Open/Closed Principle (OCP)

Bertrand Meyer: “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

abstract class Adapter {
  abstract async request<T>(url: string): Promise<T>;
}

class AjaxAdapter extends Adapter {
  constructor() {
    super();
  }
  async request<T>(url: string): Promise<T>{
    // request and return promise
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
  }
  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
  }
}

Liskov Substitution Principle (LSP)

If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program

abstract class Shape {
  render(area: number) { ... }
  abstract getArea(): number;
}

class Rectangle extends Shape {
  constructor(private readonly width = 0, private readonly height = 0) {
    super();
  }
  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(private readonly length: number) {
    super();
  }
  getArea(): number {
    return this.length * this.length;
  }
}

function renderLargeShapes(shapes: Shape[]) {
  shapes.forEach(shape => {
    const area = shape.getArea();
    shape.render(area);
  });
}

renderLargeShapes([new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]);

Good

class Rectangle {
  constructor(
    protected width: number = 0,
    protected height: number = 0) {
  }
  render(area: number) { ...}
  setWidth(width: number): void { this.width = width; }
  setHeight(height: number): void { this.height = height; }
  getArea(): number { return this.width * this.height; }
}

class Square extends Rectangle {
  setWidth(width: number): void {
    this.width = width;
    this.height = width;
  }
  setHeight(height: number): void {
    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, not 20.
    rectangle.render(area);
  });
}

renderLargeRectangles([new Rectangle(), new Rectangle(), new Square()]);

Bad

Interface Segregation Principle (ISP)

Clients should not be forced to depend upon interfaces that they do not use.

interface SmartPrinter {
  print();
  fax();
  scan();
}

class AllInOnePrinter implements SmartPrinter {
  print() {
    // ...
  }
  fax() {
    // ...
  }
  scan() {
    // ...
  }
}

class EconomicPrinter implements SmartPrinter {
  print() {
    // ...
  }  
  fax() {
    throw new Error('Fax not supported.');
  }
  scan() {
    throw new Error('Scan not supported.');
  }
}
interface Printer {
  print();
}

interface Fax {
  fax();
}

interface Scanner {
  scan();
}

class AllInOnePrinter implements Printer, Fax, Scanner {
  print() {
    // ...
  }  
  
  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements Printer {
  print() {
    // ...
  }
}

Bad

Good

Dependency Inversion Principle (DIP)

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend upon details. Details should depend on abstractions.
  • DIP is usually achieved by a using an inversion of control (IoC) container. e.g by Dependency Injection (DI)

Thanks

Clean Code in TypeScript

By gamperl

Clean Code in TypeScript

  • 308