Как стать автором
Обновить

Использование Typescript для создания react компонента «Простой фабрики»

Время на прочтение5 мин
Количество просмотров12K

Простая фабрика - довольно простой и распространенный паттерн проектирования, подробнее о котором, и не только, уже рассказано в статье VK team.

Здесь же, вы узнаете о фиче typescript, называемой "Discriminated Unions". Мы сделаем типизированный компонент фабрику.

Абстрактная задача

Создать компонент(далее "компонент-фабрика"), который, в зависимости от принимаемых им параметров, будет возвращать подходящий к ним компонент(далее "подкомпонент"). При этом учтем то, что:

  • Компонент не должен принимать таких параметров, при которых он ломается

  • Должна быть возможность расширять компонент, а именно добавлять новый подкомпонент

  • Он должен выполнять только те вычисления, которые соответвствуют возращаемому подкомпоненту

  • При использовании компонента должно быть понятно какие параметры он принимает для каждого подкомпонента и что возращает в ответ

Реальная задача

Создать компонент, который, по переданным ему параметрам, рисует подходящую фигуру.

Моделирование

Смоделируем фигуры и параметры, необходимые для их отрисовки:

  • Прямоугольник - высота и ширина

  • Круг - радиус

  • Треугольник - три стороны

У каждой фигуры уникальные параметры, но в добавок они могут иметь общий параметр — имя класса (css).

Компонент-фабрика, которую мы создадим, сможет принимать параметры всех фигур, но, чтобы он вернул нам подходящую фигуру, мы будем указывать в параметрах тип фигуры.

Как это выглядит на Javascript

Создадим абстрактные подкомпоненты (фигуры), т. к. нас в текущем контексте не интересует их реализация.

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, определяем какая это фигура. Проверяем приходят ли все необходимые для этой фигуры параметры, если да, рисуем фигуру.

Проблемы, которые возникают при использовании javascript

  1. Разработчик, используя наш компонент-фабрику, не знает какие параметры ему передать, чтобы получить нужную ему фигуру. Ему придется лазить в нашем коде, чтобы понять что к чему.

  2. Если параметры фигуры приходят из предка пятого поколения или же, к примеру, из redux store, велик шанс, что параметры станут когда-то некорректны и придется потратить уйму времени, чтобы понять, почему не отрисовалась фигура.

  3. Если передать все параметры одновременно, то фабрика отрисует фигуру, переданную в type, используя подходящие ему параметры, а остальные параметры будут проигнорированы. Это прибавит неясности в логике нашего компонента. Пример:

    Использование компонента-фабрики со всеми пропсами одновременно
    Использование компонента-фабрики со всеми пропсами одновременно
  4. Если передать некорректные параметры, узнать об этом можно только просмотрев отрисовванный результат в браузере, так как IDE нам никак не поможет.

Как же это выглядит на typescript

Для начала определим параметры, которые будут иметь все фигуры. Это имя класса, как мы уже писали в главе "Моделирование".

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
Подсказки, когда не указан type

Но так, как type обязательный параметр, то его обязательно нужно будет прописать. И как только мы это сделаем, подсказка будет показывать только подходящие параметры и typescript начнет ругаться при использовании неподходящих.

Подсказки, когда указан type
Подсказки, когда указан type
Некорректные и корректные параметры для прямоугольника
Некорректные и корректные параметры для прямоугольника

Возможно на скриншотах не совсем наглядно, но что есть, то есть. Оставляю ссылку на codesanbox, где вы можете потыкать и в живую посмотреть результат.

Вывод

Пользуясь typescript, мы получаем такие плюсы, как:

  • Избавление от лишних проверок на наличие параметров

  • Подсказки во время разработки самого компонента-фабрики

  • Подскази во время его использования

  • Предупреждение от IDE, если передавать в него некорректные параметры

На больших и долгоживущих проектах это важные преимущества, которыми нужно пользоваться.

Теги:
Хабы:
Всего голосов 5: ↑5 и ↓0+5
Комментарии13

Публикации

Истории

Работа

Ближайшие события

Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург