Практичний TypeScript. React + Redux

В даний час розробка будь-якого сучасного фронтэнд-додатка складніше рівня hello world, над яким працює команда (складу якої періодично змінюється), висуває високі вимоги до якості кодової бази. Щоб підтримувати рівень якості коду на належному рівні, ми під фронтэнд-команді #gostgroup йдемо в ногу з часом і не боїмося застосовувати сучасні технології, які показують свою практичну користь у проектах компаній самого різного масштабу.

Про статичної типізації та її користь на прикладі TypeScript було багато сказано в різних статтях і тому сьогодні ми зосередимося на більш прикладних задач, з якими стикаються фронтэнд-розробники на прикладі улюбленого нашою командою стека (React + Redux).

 

“Не розумію, як ви взагалі живете без суворої типізації. Чим займаєтеся. Дебажите цілими днями?” — не мені відомий автор.

 

“ні, пишемо цілими днями типи” — мій колега.

 

При написання коду на TypeScript (тут і далі в тексті буде матися на увазі стек сабжу) багато хто скаржиться на те, що доводиться витрачати багато часу на написання типів вручну. Хороший приклад, який ілюструє проблему, функція-коннектор connect з бібліотеки react-redux. Давайте поглянемо на код нижче:

 

type Props = {
 a: number,
 b: string;
 action1: (a: number => void;
 action2: (b: string) => void;
}

class Component extends React.PureComponent<Props> { }

connect(
 (state: RootStore) => ({
 a: state..a,
 b: state.b,
 }), {
action1,
action2,
},
)(Component);

 

В чому тут проблема? Проблема в тому, що для кожного нового властивості, інжектіруемих через конектор, ми повинні описати тип цієї властивості в загальному типі властивостей компонента (React). Не дуже цікаве заняття, скажіть ви, все-таки хочеться мати можливість збирати тип властивостей коннектора в один тип, який потім один раз “підключати” до загальним типом властивостей компонента. У мене хороша новина для вас. Вже сьогодні TypeScript дозволяє це зробити! Готові? Поїхали!

 

Потужність TypeScript

 

TypeScript не стоїть на місці і постійно розвивається (за що я його люблю). Починаючи з версії 2.8 в ньому з’явилася дуже цікава функція (conditional types), яка дозволяє виробляти маппинги типів на основі умовних виразів. Не буду вдаватися в подробиці тут, а просто залишу посилання на документацію і вставлю шматок коду з неї в якості ілюстрації:

 

type TypeName<T> =
 T extends string ? "string" :
 T extends number ? "number" :
 T extends boolean ? "boolean" :
 T extends undefined ? "undefined" :
 T extends Function ? "function" :
"object";

type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>; // "object"

 

Ця функція допомагає в нашому випадку. Подивившись в опис типів бібліотеки react-redux, можна знайти тип InferableComponentEnhancerWithProps, який відповідає за те, щоб типи інжектованих властивостей не потрапили у зовнішній тип властивостей компонента, які ми повинні явно задавати при инстанцировании компонента. У типу InferableComponentEnhancerWithProps є два узагальнених параметра: TInjectedProps і TNeedsProps. Нас цікавить перший. Давайте спробуємо витягнути цей тип з цього коннектора!

 

type TypeOfConnect<T> = T extends InferableComponentEnhancerWithProps<infer Props, infer _>
 ? Props
 : never
;

 

І безпосередньо витягування типу на реальному прикладі з репозиторію(який можна скопіювати і запустити там тестову програму):

 

import React from 'react';
import { connect } from 'react-redux';

import { RootStore, init, TypeOfConnect, thunkAction, unboxThunk } from 'src/redux';

const storeEnhancer = connect(
 (state: RootStore) => ({
...state,
 }), {
init,
 thunkAction: unboxThunk(thunkAction),
}
);

type AppProps = {}
 & TypeOfConnect<typeof storeEnhancer>
;

class App extends React.PureComponent<AppProps> {
 componentDidMount() {
this.props.init();
this.props.thunkAction(3000);
}
 render() {
 return (
<>
<div>{this.props.a}</div>
<div>{this.props.b}</div>
<div>{String(this.props.c)}</div>
</>
);
}
}

export default storeEnhancer(App);

 

У наведеному вище прикладі ми ділимо підключення до сховища (Redux) на два етапи. На першому етапі ми отримуємо компонент вищого порядку storeEnhancer (він же тип InferableComponentEnhancerWithProps) для вилучення з нього інжектіруемих типів властивостей за допомогою нашого типу-помічника TypeOfConnectі подальшого об’єднання (через интерсекцию типів &) отриманих типів властивостей з власними типами властивостей компонента. На другому етапі ми просто декоруємо наш вихідний компонент. Тепер, що б ви не додали в конектор, автоматично буде потрапляти в типи властивостей компонента. Здорово, те, чого ми хотіли досягти!

 

Уважний читач помітив, що генератори екшенів (для стислості надалі за текстом спростимо до терміна екшену) з сайд-ефектами (thunk action creators) проходять додаткову обробку з допомогою функції unboxThunk. Чим же викликана така додаткова міра? Давайте розбиратися. Спочатку подивимося на сигнатуру такого екшену на прикладі програми з репозиторію:

 

const thunkAction = (delay: number): ThunkAction<void, RootStore, void, AnyAction> => (dispatch) => {
 console.log('waiting for', delay);
 setTimeout(() => {
console.log('reset');
dispatch(reset());
 }, delay);
};

 

Як видно з сигнатури, наш екшен не відразу повертає цільову функцію, а спочатку проміжну, яку підхоплює redux-middleware для можливості твори сайд-ефектів в нашій основної функції. Але при використанні цієї функції в підключеному вигляді властивості компонента, сигнатура цієї функції скорочується, виключаючи проміжну функцію. Як це описати в типах? Потрібна спеціальна функція-перетворювач. І знову TypeScript показує свою міць. Спочатку опишемо тип, який прибирає проміжну функцію з сигнатури:

 

CutMiddleFunction<T> = T extends (...arg: infer Args) => (...args: any[]) => R infer
 ? (...arg: Args) => R
 : never
;

 

Тут, крім умовних типів, використовується зовсім свіже нововведення з TypeScript 3.0, який дозволяє виводити тип довільного (rest parameters) кількості аргументів функції. Подробиці дивіться в документації. Тепер залишається вирізати з нашого екшену зайву частину досить жорстким чином:

 

const unboxThunk = <Args extends any[], R, S, E, A extends Action<any>>(
 thunkFn: (...args: Args) => ThunkAction<R, S, E, A>
) => (
 thunkFn as any as CutMiddleFunction<typeof thunkFn>
);

 

Пропустивши екшен через такий перетворювач, ми на виході маємо потрібну нам сигнатуру. Тепер екшен готовий для його використання в коннекторе.

 

Ось так, шляхом нехитрих маніпуляцій, ми скорочуємо наш ручна праця при написанні типізованого коду на нашому стеку. Якщо піти трохи далі, то можна також спростити типизирование екшенів і редьюсеров, як ми це зробили в redux-modus.

 

P. S. При використанні динамічної прив’язки екшенів в коннекторе через функцію і redux.bindActionCreators потрібно будемо самому подбати про більш правильною типізації цієї утиліти (можливо через написання своєї обгортки).

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *