Clean Code for TypeScript - Part 3

The final part of the articles is about how you can use the principles of pure code in TypeScript (ps. All these principles apply not only to the TypeScript language).



Testing


. , , . , 100% . , , .


, . JS TypeScript, . , , /, . - (TDD), , β€” , , - .


TDD


  1. , , .
  2. , , ( , , ).
  3. , , .

F.I.R.S.T.


:


  • (Fast) . , , , β€œβ€, , . , .
  • (Independent) . , .
  • (Repeatable) , , .
  • (Self-Validating) Passed, Failed. , , .
  • (Timely) . , , .


(SPP). .(ps. )


import { assert } from 'chai';

describe('AwesomeDate', () => {
  it('handles date boundaries', () => {
    let date: AwesomeDate;

    date = new AwesomeDate('1/1/2015');
    assert.equal('1/31/2015', date.addDays(30));

    date = new AwesomeDate('2/1/2016');
    assert.equal('2/29/2016', date.addDays(28));

    date = new AwesomeDate('2/1/2015');
    assert.equal('3/1/2015', date.addDays(28));
  });
});

:


import { assert } from 'chai';

describe('AwesomeDate', () => {
  it('handles 30-day months', () => {
    const date = new AwesomeDate('1/1/2015');
    assert.equal('1/31/2015', date.addDays(30));
  });

  it('handles leap year', () => {
    const date = new AwesomeDate('2/1/2016');
    assert.equal('2/29/2016', date.addDays(28));
  });

  it('handles non-leap year', () => {
    const date = new AwesomeDate('2/1/2015');
    assert.equal('3/1/2015', date.addDays(28));
  });
});


promises callbacks


Callback- ( (callback hell)). , , callback-, , ( Node.js util.promisify, pify, es6-promisify)


:


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

:


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

, :


Promise.resolve(value).
Promise.reject(error).
Promise.all(promises), , .
Promise.race(promises), / / .

Promise.all , . Promise.race , - .



β€” ! , , - , ( Node) .


(reject)


JavaScript TypeScript throw . . throw Error. catch. . Error.


:


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

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

:


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

Error , try/catch/finally stack, . : throw . TypeScript .
:


type Result<R> = { isError: false, value: R };
type Failure<E> = { isError: true, error: E };
type Failable<R, E> = Result<R> | Failure<E>;

function calculateTotal(items: Item[]): Failable<number, 'empty'> {
  if (items.length === 0) {
    return { isError: true, error: 'empty' };
  }

  // ...
  return { isError: false, value: 42 };
}

.



- . (console.log) , . try/catch , .


:


try {
  functionThatMightThrow();
} catch (error) {
  console.log(error);
}

// or even worse

try {
  functionThatMightThrow();
} catch (error) {
  // ignore error
}

:


import { logger } from './logging'

try {
  functionThatMightThrow();
} catch (error) {
  logger.log(error);
}

,


, try/catch.


:


getUser()
  .then((user: User) => {
    return sendEmail(user.email, 'Welcome!');
  })
  .catch((error) => {
    console.log(error);
  });

:


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


. , , . β€” . . ! . ,


TypeScript TSLint. , . ESLint, TSLint .
TSLint ESLint, :



TypeScript StyleGuide and Coding Conventions .



, … , , . , , , .


:


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() {}

type animal = { /* ... */ }
type Container = { /* ... */ }

:


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() {}

type Animal = { /* ... */ }
type Container = { /* ... */ }

PascalCase , , . camelCase , .



.
, import:


  • .
  • .
  • (.. import {A, B, C} from 'foo';)
  • , ..: import * as foo from 'a'; import * as bar from 'b';
  • .
  • :
    • (.. import 'reflect-metadata';)
    • Node (.. import fs from 'fs';)
    • External modules (i.e. import { query } from 'itiriri';)
    • Internal modules (i.e. import { UserService } from 'src/services/userService';)
    • Modules from the parent directory (i.e. import foo from '../foo'; import qux from '../../foo/qux';)
    • Modules from the same or related catalog (i.e. import bar from './bar'; import baz from './bar/baz';)

Poorly:


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';

Good:


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';

Use typescript aliases


Create a prettier import by defining the paths and baseUrl properties in the compilerOptions section of tsconfig.json
This will avoid long relative paths when importing.


Poorly:


import { UserService } from '../../../services/UserService';

Good:


import { UserService } from '@services/UserService';

// tsconfig.json
...
  "compilerOptions": {
    ...
    "baseUrl": "src",
    "paths": {
      "@services": ["services/*"]
    }
    ...
  }
...

PS


The first part
The second part
Full translation

Source: https://habr.com/ru/post/undefined/


All Articles