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
- Do not optimise.
- 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