across application layers

Michał Michalczuk / Bartosz Cytrowski

github repository link

Michał
Michalczuk

Bartosz
Cytrowski

 

@cytrowski

@michalczukm

TypeScript

JavaScript that scales

TypeScript

ES6

ES5

  • Compiles to JS
     
  • JavaScript is a valid TypeScript as well
     
  • Optionally statically typed

myths and legends

Myth 1

TS works for you only if the whole project is in TS

Myth 2

babel or tsc -
it no difference

class Task {
  private _title = '';
  constructor(title: string) {
    this._title = title;
  }

  get title() {
    return this._title;
  }

  set title(value) {
    const formattedTitle = value.trim();
    if (formattedTitle === '') {
      throw TypeError('Title cannot be empty');
    }
    this._title = formattedTitle;
  }
}
task.ts
var Task = /** @class */ (function () {
    function Task(title) {
        this._title = '';
        this._title = title;
    }
    Object.defineProperty(Task.prototype, "title", {
        get: function () {
            return this._title;
        },
        set: function (value) {
            var formattedTitle = value.trim();
            if (formattedTitle === '') {
                throw TypeError('Title cannot be empty');
            }
            this._title = formattedTitle;
        },
        enumerable: true,
        configurable: true
    });
    return Task;
}());
"use strict";

function _instanceof(left, right) {
  if (
    right != null &&
    typeof Symbol !== "undefined" &&
    right[Symbol.hasInstance]
  ) {
    return right[Symbol.hasInstance](left);
  } else {
    return left instanceof right;
  }
}

function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

var Task =
  /*#__PURE__*/
  (function() {
    function Task(title) {
      _classCallCheck(this, Task);

      _defineProperty(this, "_title", void 0);

      this._title = title;
    }

    _createClass(Task, [
      {
        key: "title",
        get: function get() {
          return this._title;
        },
        set: function set(value) {
          var formattedTitle = value.trim();

          if (formattedTitle === "") {
            throw TypeError("Title cannot be empty");
          }

          this._title = formattedTitle;
        }
      }
    ]);

    return Task;
  })();

Myth 3

My IDE don't know TS

Myth 4

TS guards your types only at compilation time
npm install runtypes

❤️ community

myths and legends

Why do we type in JS

?

is eating
the world

Where is it already

?

the classic one

the twisted classic

the all in

BFF

 

?

Who the hell is Kate

Problem 1.

Fragile contracts

~/api/products/1

{
  "id": 1,
  "name": "Brown bag",
  "availability": true
}
{
  "_ID": 1,
  "name": "Brown bag",
  "isAvailable": true
}
type Product = {
    id: number;
    name: string;
    availability: boolean;
}
type Product = {
    _ID: number;
    name: string;
    isAvailable: boolean;
}
{
  "id": 1,
  "name": "Brown bag",
  "availability": true
}

What will Kate do?

~/api/products/1

{
  "id": 1,
  "name": "Brown bag",
  "availability": true
}
{
  "_ID": 1,
  "name": "Brown bag",
  "isAvailable": true
}
type Product = {
    id: number;
    name: string;
    availability: boolean;
}
{
  "id": 1,
  "name": "Brown bag",
  "availability": true
}
{
  "_ID": 1,
  "name": "Brown bag",
  "isAvailable": true
}
type Product = {
    _ID: number;
    name: string;
    isAvailable: boolean;
}

Easy? forget it

Simple? yep!

What's the Kate's secret souce?

monorepo

Lerna

Bolt

Nx

How to be like Kate?

Show me the code!

import * as React from 'react';
import { Component } from 'react';
import { Todo } from '@trails/typings';

import './app.css';

interface State {
  todos: Todo[]
}

export class App extends Component<{}, State> {
  // ...

front-end (React)

import { 
  Controller, Get, Post, Body 
} from '@nestjs/common';

import { Todo, Product } from '@trails/typings';

import { TodosService } from './todos.service';

@Controller()
export class AppController {
  constructor(
    private readonly todosService: TodosService
  ) { }

  @Get('todos')
  async getTodo(): Promise<Todo[]> {
    return this.todosService.get();
  }
  // ...

back-end (NestJS)

export interface Todo {
  id: number;
  title: string;
}

type

{
  "compilerOptions": {
    "paths": {
      "@trails/typings": [
        "libs/typings/src/index.ts"
      ]
    }
  }
}

tsconfig.json

What if our backend is not TS-ified?

are we doomed?

Swagger can do stuff

What if we don't have Swagger :(

should we cry in the corner?

quicktype.io

quicktype.io

json2ts.com

TypeScriptDefinitionGenerator

for VSCode

Problem 2.

Boilerplate in models

Presentation/Delivery

Logic

Data Access

class Product {
    public int Id { get; set; }
    public string Name { get; set; }
}
class ProductModel {
    public int Id { get; set; }
    public string Name { get; set; }
}
class ProductApiModel {
    public int Id { get; set; }
    public string Name { get; set; }
}
ProductModel Map(Product product) {
    return new ProductModel {
        Id = product.Id,
        Name = product.Name
    }
}

Presentation/Delivery

Logic

Data Access

type Product = {
    id: number;
    name: string;
}
type ProductModel = Product;
type ProductApiModel = ProductModel;
type ProductApiModel = ProductModel & {
    isDraft: boolean;
    canSave: boolean;
};



// result type
{
  id: number;
  name: string;
  isDraft: boolean;
  canSave: boolean;
}

Extra fields

type ProductApiModel = 
    Omit<ProductModel, 'name'>


// result type
{
  id: number;
}

Omit some fields

type ProductApiModel = Partial<ProductModel>;



// result
{
  id?: number;
  name?: string;
}

Make fields optional

type ProductApiModel = Readonly<ProductModel>;



// result
{
  readonly id: number;
  readonly name: string;
}

Make fields readonly

Dummy components

Container components

Data Access: API/Store

Presentation/Delivery

Logic

Data Access

Dummy components

Container components

Data Access: API/Store

Presentation/Delivery

Logic

Data Access

HTTP

Problem 3.

Run-time type validation

type Payment = {
  id: number;
  vendorId: number;
  clientId: number;
  currency?: string;
  amount: number;
}
type NotWhatYouExpected = {
  id?: number;
  vendorId: number;
  payerId: number;
  currencyISO: string;
  value: string;
}
export interface Payment {
  id: number;
  vendorId: number;
  clientId: number;
  currency?: string;
  amount: number;
}
async get(): Promise<Payment[]> {
    return this.httpService
      .get<Payment[]>(`${this.externalServiceUrl}/payments`)
      .pipe(
        map(response => response.data)
      )
      .toPromise();
  }
payments.service.ts
payments.model.ts
[
  { 
    id: 5, 
    vendorId: 100, 
    payerId: 200, 
    currencyISO: 'PLN', 
    value: '1000' 
  },
  { 
    vendorId: 101, 
    payerId: 201, 
    currencyISO: 'EUR', 
    value: '1001' 
  }
]
real live response
[
  { 
    id: 5, 
    vendorId: 100, 
    clientId: 200, 
    currency: 'PLN', 
    amount: 1000
  },
  { 
    id: 6,
    vendorId: 101, 
    clientId: 201, 
    currency: 'EUR', 
    amount: 1001
  }
]

Compilation

Run-time

if (isDuck(pet)) {
    pet.quack();
} else {
    throw new Error(`OMG it's not a duck!`);
}

/// ---------

function isDuck(pet: Duck | Fish): pet is Duck {
    return (<Duck>pet).quack !== undefined;
}


Type Guards

npm install runtypes
import { 
  Record, Number, String, 
  Undefined, Static 
} from 'runtypes';


export const PaymentRecord = Record({
  id: Number,
  vendorId: Number,
  clientId: Number,
  currency: String.Or(Undefined),
  amount: Number
});

export type Payment = Static<typeof PaymentRecord>;
payments.model.ts
export type Payment = Static<typeof PaymentRecord>;

// result type
{
    id: number,
    vendorId: number,
    clientId: number,
    currency?: string,
    amount: number
}
payments.model.ts
async get(): Promise<Payment[]> {
    return this.httpService
      .get<Payment[]>(`${this.externalServiceUrl}/payments`)
      .pipe(
        map(response => response.data),
        tap(payment => PaymentRecord.check(payment))
      )
      .toPromise();
  }
payments.service.ts
[Nest] 36664   - 5/4/2019, 5:06:41 PM   
[ExceptionsHandler] Expected number, 
          but was undefined +2061065ms

ValidationError: Expected number, 
          but was undefined

Summary time!

+ some extra advices

How to start with      ?

  • Check if your external dependencies are typed 
  • Structure is the king - monorepo (Nx rox!)
  • While rewriting - go bottom-up

What to remember?

  • You don't have to rewrite your app in one shot
  • TS does not validate types in the run-time...
  • ...`runtypes` does
  • Share types across layers
  • Do not be scared of monorepo
  • Beware of the boilerplate
  • Kate rules!

Thank you :)

michalczukm

cytrowski

TypeScript across application layers

By Michał Michalczuk

More from Michał Michalczuk