na styku warstw aplikacji

Michał Michalczuk / Bartosz Cytrowski

Michał
Michalczuk

Bartosz
Cytrowski

 

@cytrowski

@michalczukm

TypeScript

JavaScript that scales

TypeScript

ES6

ES5

  • Kompiluje się do JS
     
  • JavaScript jest legalnym TypeScript
     
  • Opcjonalnie statycznie typowany

mity i legendy

Mit 1

TS sprawdza się tylko wtedy, gdy wszystko jest w TS

Mit 2

babel czy tsc - nie robi różnicy

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

Mit 3

Moje IDE nie umie w TS-a

Mit 4

TS pilnuje typów tylko podczas kompilacji
npm install runtypes

❤️ community

mity i legendy

Dlaczego typujemy

?

is eating
the world

Gdzie już jest

?

Klasyk

Klasyk z twistem

Full TS

BFF

 

?

Kim jest Kasia

Problem 1.

Wrażliwe kontrakty

~/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
}

Co na to Kasia?

~/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;
}

Proste? niekoniecznie

Łatwe? tak!

Jak zrobiła to Kasia?

monorepo

Lerna

Bolt

Nx

Chcesz być jak Kasia?

Jak to wygląda w kodzie?

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

typ

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

tsconfig.json

Co jeżeli backend nie używa TS?

are we doomed?

Swagger umie w TS-a

Ale ja nie mam Swaggera ;(

co począć?

quicktype.io

quicktype.io

json2ts.com

TypeScriptDefinitionGenerator

for VSCode

Problem 2.

Boilerplate modeli danych

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 ProductModel = Product;

type ProductApiModel = ProductModel & {
    isDraft: boolean;
    canSave: boolean;
};



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

Dodatkowe pola

type ProductModel = Product;

type ProductApiModel = Pick<
    ProductModel, Exclude<keyof ProductModel, 'name'>
>;



// result
{
  id: number;
}

Omiń pola

type ProductModel = Product;

type ProductApiModel = Partial<ProductModel>;



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

Opcjonalne pola

type ProductModel = Product;

type ProductApiModel = Readonly<ProductModel>;



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

Pola 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.

Brak walidacji typów w run-time

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

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


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

if (isDuck(pet)) {
    pet.quack();
}

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

Wnioski

...i kilka dobrych rad ;)

Od czego zacząć?

  • Sprawdź, czy na npm dostępne są typingi do Twoich zewnętrznych zależności
  • Zadbaj o strukturę - wprowadź monorepo (polecamy Nx)
  • Przepisując, zaczynaj od liści, czyli modułów posiadających najmniej zależności

Co zapamiętać?

  • TypeScript można wprowadać iteracyjnie
  • TS nie waliduje typów w run-timie.
  • `runtypes` pozwala walidować dane w run-time
  • Współdziel lub synchronizuj typy działające pomiędzy warstwami
  • Nie bój się monorepo
  • Unikaj boilerplate'u
  • Kasia jest spoko

Dzięki :)

michalczukm

cytrowski

TypeScript na styku warstw aplikacji

By Michał Michalczuk

TypeScript na styku warstw aplikacji

Repo: https://github.com/infoshareacademy/infoshare-2019-typescript-between-layers

  • 1,674