Использование Typescript для создания react компонента «Простой фабрики»
- пятница, 11 февраля 2022 г. в 00:37:56
Простая фабрика - довольно простой и распространенный паттерн проектирования, подробнее о котором, и не только, уже рассказано в этой статье.
Суть данной статьи в раскрытии одной из возможностей typescript, которым мало кто пользуется, на примере компонента "фабрики".
Создать компонент(далее "компонент-фабрика"), который, в зависимости от принимаемых им параметров, будет возвращать подходящий к ним компонент(далее "подкомпонент"). При этом учтем то, что:
Компонент не должен принимать таких параметров, при которых он ломается
Должна быть возможность расширять компонент, а именно добавлять новый подкомпонент
Он должен выполнять только те вычисления, которые соответвствуют возращаемому подкомпоненту
При использовании компонента должно быть понятно какие параметры он принимает для каждого подкомпонента и что возращает в ответ
Создать компонент, который, по переданным ему параметрам, рисует подходящую фигуру.
Смоделируем фигуры и параметры, необходимые для их отрисовки:
Прямоугольник - высота и ширина
Круг - радиус
Треугольник - три стороны
У каждой фигуры уникальные параметры, но в добавок они могут иметь общий параметр — имя класса (css).
Компонент-фабрика, которую мы создадим, сможет принимать параметры всех фигур, но, чтобы он вернул нам подходящую фигуру, мы будем указывать в параметрах тип фигуры.
Создадим абстрактные подкомпоненты (фигуры), т. к. нас в текущем контексте не интересует их реализация.
const Rectangle = ({className, width, height}) => ...;
const Circle = ({className, radius}) => ...;
const Triangle = ({className, sideA, sideB, sideC}) => ...;
Переходим к компоненту фабрике.
const Figure = ({
className,
type,
width,
height,
radius,
sideA,
sideB,
sideC,
}) => {
switch (type) {
case "rectangle":
return height && width ? (
<Rectangle className={className} height={height} width={width} />
) : null;
case "circle":
return radius ? <Circle className={className} radius={radius} /> : null;
case "triangle":
return sideA && sideB && sideC ? (
<Triangle
className={className}
sideA={sideA}
sideB={sideB}
sideC={sideC}
/>
) : null;
default:
return null;
}
};
По переданному параметру type, определяем какая это фигура. Проверяем приходят ли все необходимые для этой фигуры параметры, если да, рисуем фигуру.
Разработчик, используя наш компонент-фабрику, не знает какие параметры ему передать, чтобы получить нужную ему фигуру. Ему придется лазить в нашем коде, чтобы понять что к чему.
Если параметры фигуры приходят из предка пятого поколения или же, к примеру, из redux store, велик шанс, что параметры станут когда-то некорректны и придется потратить уйму времени, чтобы понять, почему не отрисовалась фигура.
Если передать все параметры одновременно, то фабрика отрисует фигуру, переданную в type, используя подходящие ему параметры, а остальные параметры будут проигнорированы. Это прибавит неясности в логике нашего компонента. Пример:
Если передать некорректные параметры, узнать об этом можно только просмотрев отрисовванный результат в браузере, так как IDE нам никак не поможет.
Для начала определим параметры, которые будут иметь все фигуры. Это имя класса, как мы уже писали в главе "Моделирование".
interface IBaseFigure {
className?: string;
}
Создадим те же абстрактные подкомпоненты, что и в варианте с javascript, но теперь с типизацией.
interface ICircle extends IBaseFigure {
radius: number;
}
const Circle: React.VFC<ICircle> = ({className, radius}) => ...;
interface IRectangle extends IBaseFigure {
width: number;
height: number;
}
const Rectangle: React.VFC<IRectangle> = ({className, width, height}) => ...;
interface ITriangle extends IBaseFigure {
sideA: number;
sideB: number;
sideC: number;
}
const Triangle: React.VFC<ITriangle> = ({className, sideA, sideB, sideC}) => ...;
);
Мы создали интерфейс BaseFigure, а потом расширяем его интерфейсами наших фигур с помощью "extend". Если мы определим новый параметр в BaseFigure, то он появится во всех интерфейсах расширяюших его.
В интерфейсах мы также указали какого типа должны быть параметры, className должен быть строкой и является необязательным, а остальные параметры обязательны и должны быть числом.
Сами подкомпоненты мы протипизировали с помощью встроенного в React типа VFC, который означает "Функциональный компонент без children".
В целом такого "определения" будет достаточно в рамках этой статьи.
Протипизируем также нашу фабрику.
interface IFigureRectangle extends IRectangle {
type: "rectangle";
}
interface IFigureCircle extends ICircle {
type: "circle";
}
interface IFigureTriangle extends ITriangle {
type: "triangle";
}
type IFigure = IFigureRectangle | IFigureCircle | IFigureTriangle;
const Figure: React.VFC<IFigure> = (props) => {
switch (props.type) {
case "rectangle": {
return <Rectangle {...props} />;
}
case "circle": {
return <Circle {...props} />;
}
case "triangle": {
return <Triangle {...props} />;
}
default:
return null;
}
};
Сначала расширяем интерфейсы фигур, добавляя к ним свойство type с определенным значением. Если type задать такое значение, то остальные параметры компонента определятся по этому интерфейсу.
Мы объединяем(строка 13) такие интерфейсы и получаем единый интерфейс, где в случае, если параметр type равно:
rectangle, остальныe параметры будут из IRectangle
circle, остальные параметры будут из ICircle
triangle, остальные параметры будут из ITriangle
Теперь, благодаря типизации, компонент-фабрика решила проблемы, которые были в варианте с использованием js. Также у нас отпала необходимость проверять приходят ли все параметры.
Мы можем внутри "case"-ов узнавать, какие параметры будут получены.
При использовании, сначала нам в подсказках будут показываться все пропсы.
Но так, как type обязательный параметр, то его обязательно нужно будет прописать. И как только мы это сделаем, подсказка будет показывать только подходящие параметры и typescript начнет ругаться при использовании неподходящих.
Возможно на скриншотах не совсем наглядно, но что есть, то есть. Оставляю ссылку на codesanbox, где вы можете потыкать и в живую посмотреть результат.
Пользуясь typescript, мы получаем такие плюсы, как:
Избавление от лишних проверок на наличие параметров
Подсказки во время разработки самого компонента-фабрики
Подскази во время его использования
Предупреждение от IDE, если передавать в него некорректные параметры
На больших и долгоживущих проектах это важные преимущества, которыми нужно пользоваться.