Безжалостная типизация

Обо мне

Фронтендер в Контур.Гособлако

Люблю ходить в горы

В прошлом бэкендер

Контур.Гособлако

  • «Стартап»
  • Работа ведётся в сжатые
  • Требование делать быстро и хорошо
  • Переобуваемся на лету

    о

сраки

  • Модульная учётная система
  • Цель — захватить мир рынок госов
  • Старт проекта — январь 2017
  • 8 бэкендеров, 1 фронтендер

Почему выбрали типизацию

Что хотели получить от типизации

  • Сделать код надежнее
  • Упростить рефакторинг
  • Избежать детских проблем в JS

Проблемы в JS

1. Опечатки

case 'SEARCH_PAGE_UPDATE_RESLUTS'

action.payload.qeury

chengeQueryText()

// ловим ошибки в рантайме
// или пишем много тупых тестов

2. Неизвестные параметры событий

// какие параметры принимает onSearch?

onSearch(query)

onSearch({query})

onSearch(() => {...})

3. Непредсказуемые входные параметры

// что прийдёт в «options»?

const onSearch = (options) => {   
    // options.query? 
    ...
}

4. Проблемы после обновления npm пакетов

import Input from 'ui/Input';

<Input
  size="large"
  width={600}
  value={props.query}
  onUpdate={props.onUpdate} 
  ...
/>

// поменялось имя свойства onUpdate → onChange

5. Нет IntelliSense

Решение — типизировать

  • Опечатки
  • Неизвестные параметры событий
  • Непредсказуемые входные параметры
  • Проблемы после обновления npm пакетов
  • Нет IntelliSense

FIXED

Мы выбрали TypeScript

TypeScript

  • Язык со статической типизацией
  • Компилируется в JS (ES3, ES5, ES6)
  • Понятен для бэкендеров*
  • Динамично развивается c 2012
  • Отличная поддержка в IDE

TypeScript

JS

React

Redux

  • Популярная реализация FLUX архитектуры
  • Контейнер состояния
  • Прост и предсказуем в работе

Действия

Состояние

Компоненты

Events

State

Redux Flow

Actions

Есть один нюанс

Печально когда нет типизации

1. Неизвестные параметры в props

const SearchPage = (props) => (  
  <SearchBar
    // что такое props.onSearch?
    // какие параметры принимает
    onSearch={props.onSearch}
  />
  ...
);

2. Непонятно что принимает reducer

const reducer = (
  previousState, // ← что здесь?
  action // ← а здесь?
) => {
  ...
};

3. Что передается в action

const reducer = (
  previousState,
  action
) => {
  switch (action.type) {        
   сase 'SEARCH_PAGE_UPDATE_RESULTS':
     return {
        ...state,
        // а точно ли query?
        query: action.query 
     }
   ...

4. Константы action.type

const reducer = (
  previousState,
  action
) => {
  switch (action.type) {        
   // какие значения 
   // может принимать action.type?
   сase 'SEARCH_PAGE_UPDATE_RESULTS':
   сase Constants.SEARCH_PAGE_CHANGE_QUERY:
     ...
  }
};

5. Непонятно что в app state

const rootReducer = combineReducers({ 
    searchPageReducer,
    authReducer,
    ... 
});

// из чего состоит state приложения
// из каких редьюсеров

Что будем делать?

  1. Будем жить с этим
  2. Затипизируем

Затипизируем

Инструменты

Подготовка файлов

  • js → ts
  • jsx → tsx
  • tsconfig.json
  • babel-loader → ts-loader

Чуть-чуть теории

TypeScript basic types

  • Boolean, Number, String, Array
  • Tuple, Enum, Any, Void, Null, Undefined, Never
let isDone: boolean;
let query: string;
let onSearch: (query: string) => void;
let list: any[] = [1, true, 'free'];

TypeScript advanced types

  • Union Types
    • number | string | null
  • String Literal Types
    • 'SEARCH_PAGE_CHANGE_QUERY'
  • Type Aliases
    • type fn = () => void;

TypeScript interfaces

interface Action {
  type: string;
  payload: any;
}

// пример
const anyAction: Action = {
  type: 'ANY_ACTION',
  payload: 0
}

TypeScript generics

interface Action<T> {
  type: string;
  payload: T;
}

// пример
const numberAction: Action<number> = {
  type: 'NUMBER_ACTION',
  payload: 0
}

Redux v4 подружился с Generic Types

interface Action<T = any> {
  type: T;
}

type Reducer<
    S = any, 
    A extends Action = AnyAction
> = (state: S | undefined, action: A) => S;

...

redux/index.d.ts

План типизации

  • Events
    • ​параметры функций
    • component props
  • Actions
    • action.​type
    • action.​payload
  • State
    • app state
    • ​reducer state
    • selectors
    • action types, action.payload

Events & Props

interface Props {
  query: string;
  onSearch: (options: SearchParams) => void;
}

const SearchPage: React.SFC<Props> = props => (  
  <SearchBar
    query={props.query}
    onSearch={props.onSearch}
  />
)
            

Типизируем props в компонентах

Автокомплит props в компонентах

interface Actions {
  onSearch: (options: SearchParams) => void;
}

const searchPageActions: Actions = {
  onSearch: searchRequest
};

const SearchContainer = connect(
  searchPageSelector, 
  searchPageActions
)(SearchPage);

// connect - связывает React и Redux

Типизируем события в connect

Что получили

  • Зафиксированный контракт  
  • Понятно что в props
  • Автокомплит props в компонентах
  • «Упадём» если контракт изменится

План типизации

  • Events
    • ​параметры функций
    • component props
  • Actions
    • action.​type
    • action.​payload
  • State
    • app state
    • ​reducer state
    • selectors
    • action types, action.payload

Actions

Action

interface Action<TPayload, Type> {
  readonly type: Type;
  payload: TPayload;
//error: boolean;
//meta: any;
}

type

payload

payload

Объект Action

Версия без типизации

const SEARCH_PAGE_REQUEST = 'SEARCH_PAGE_REQUEST';

const searchRequest = (options) => ({
  type: SEARCH_PAGE_REQUEST,
  payload: {
    ...options
  }
});

Первое приближение с типами

interface SearchRequestPayload {
  query: string;
}

type SearchRequestAction = Action<
  SearchRequestPayload,
  'SEARCH_PAGE_REQUEST'
>;

const SEARCH_PAGE_REQUEST = 'SEARCH_PAGE_REQUEST';

const searchRequest = (
  options: SearchRequestPayload
): SearchRequestAction => ({
  type: SEARCH_PAGE_REQUEST,
  payload: {
    ...options
  }
});

Тыж программист

function actionCreator<
  T extends Action<any, string>
>(Ctor: { new (payload: T['payload']): T }) {
  return (payload: T['payload']): T 
            => new Ctor(payload);
}

// Возвращает Action через конструктор Ctor

Функция помощник actionCreator

Функция помощник actionType

function actionType<Payload, Type>(type: Type) {
  return class implements Action<Payload, Type> {
    static readonly Type = type;
    public readonly type = type;
    constructor(public payload: Payload) {
      return {
        payload,
        type
      };
    }
  };
}

// Возвращает Action из конструктора
// Хранит константу action.type в Type

Пример использования

class SearchRequest extends actionType<
  { query: string },
  'SEARCH_PAGE_REQUEST'
>('SEARCH_PAGE_REQUEST') {}

const searchRequest = actionCreator(SearchRequest);

Что получили

  • Конструктор действий → { type, payload }
  • Вывод типа payload для каждого action.type

План типизации

  • Events
    • ​параметры функций
    • component props
  • Actions
    • action.​type
    • action.​payload
  • State
    • app state
    • ​reducer state
    • selectors
    • action types, action.payload

State

const rootReducer = combineReducers<
  AppState,
  AppActions
>({ searchPageReducer, authReducer });


interface AppState {
  searchPageReducer: SearchPageState;
  authReducer: AuthState;
}

type AppActions = SearchPageActions | AuthActions;

Затипизируем rootReducer

Типизируем reducer state

interface State {
  readonly query: string;
  readonly searchResult: SearchResult;
}

const searchPageReducer = (
  previousState: State,
  action
): State => {
  ...
};

Типизируем reducer state selector

const getState = (
  state: State, 
  ownProps: OwnProps
): State => ({
  ...state,
  ...ownProps
});

Типизируем action types

const searchPageReducer = (
  previousState: State,
  action: AppActions
): State => {
  switch (action.type) {
    ...
  }
};

Автокомплит action types

Типизированный action.payload

const searchPageReducer = (
  previousState: State,
  action: AppActions
): State => {
  switch (action.type) {
    case 'SEARCH_PAGE_UPDATE_RESULTS':
      return {
        ...state,
        searchResult: action.payload
        // (property) payload: SearchResult
      };
  }
};

// IDE — по типу action выводит тип payload

Вывод типов action.payload

Что получили

  • State всего приложения типизирован
  • Каждый reducer типизирован
    • previousState: State
    • action: AppActions 
  • Типизированные селекторы состояния
  • Автокомплит action.type в reducer
  • Вывод типа action.payload

План типизации

  • Events
    • ​параметры функций
    • component props
  • Actions
    • action.​type
    • action.​payload
  • State
    • app state
    • ​reducer state
    • selectors
    • action types, action.payload

Бонусы

  • не нужно тестировать props
  • можно затипизировать css свойства

Автокомплит css свойств

Ещё бонусы

Сколько стоит?

1. Нужно разбираться в типах

function actionCreator<
  T extends Action<any, string>
>(Ctor: { new (payload: T['payload']): T }) {
  return (payload: T['payload']): T => new Ctor(payload);
}

interface Action<TPayload, Type> {
  readonly type: Type;
  payload: TPayload;
  meta: any;
  error: boolean;
}

2. Проблемы обновления npm пакетов

  • Пакет обновился, а d.ts нет
  • Ошибки в d.ts файлах

3. Увеличивается время разработки

  • Нужно писать контракты

  • Требуется время на компиляцию

Когда применять

  • Система будет часто изменяться
  • Требуется высокое качество + скорость
  • В крупных системах

Создатель: Turbo Pascal, Delphi, C#, Typescript

Call to Action

  • Начните писать типы
  • Внесите вклад в типизацию

TS

JS

А что с FLOW ?

TypeScript vs Flow 2017/18

TypeScript

  • Компилятор
  • Нужно писать типы
  • Умеет в ES3, ES5, ES6
  • Инструменты ООП и ФП
  • Экспериментальные фичи из коробки 

Flow

  • Анализатор
  • Можно не писать типы
  • Создан для легаси

Синхронизация моделей DTO 

Web API

CLIENT

models.d.ts

  • Автогенерация контрактов с backend

  • Model.cs → interface Model { ... }

Model.cs → interface Model { ... }

public class AddressFullObjectM
{
    public AddressM address { get; set; }
    public string postalCode { get; set; }
    public HouseM house { get; set; } 
    public string building { get; set; }
    public string room { get; set; }
}
interface AddressFullObjectM {
  address: server.AddressM;
  building: string;
  house: server.HouseM;
  postalCode: string;
  room: string;
}

AddressFullObjectM.cs

AddressFullObjectM.d.ts

Контакты

shatikhin@skbkontur.ru

2018

Михаил Шатихин

Пример типизации:

Безжалостная типизация

By Mikhail Shatikhin

Безжалостная типизация

  • 1,423