Безжалостная типизация
Обо мне
Фронтендер в Контур.Гособлако
Люблю ходить в горы
В прошлом бэкендер
Контур.Гособлако
- «Стартап»
- Работа ведётся в сжатые
- Требование — делать быстро и хорошо
- Переобуваемся на лету
о
- Модульная учётная система
- Цель — захватить
миррынок госов - Старт проекта — январь 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
- Библиотека для построения интерфейса
- Готовые kontur react-ui компоненты
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 приложения
// из каких редьюсеров
Что будем делать?
- Будем жить с этим
- Затипизируем
Затипизируем
Инструменты
Подготовка файлов
- 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
-
A
ctionsaction.typeaction.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
-
A
ctionsaction.typeaction.payload
-
Stateapp statereducer stateselectorsaction 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,436