Как расширить свойства HTML-элемента в TypeScript?

Для чего используется TypeScript Программирование и разработка

В этом кратком совете, взятом из книги «Раскрытие возможностей TypeScript», Стив показывает, как расширить свойства элемента HTML в TypeScript.

В большинстве крупных приложений и проектов, над которыми я работал, мне часто приходилось создавать кучу компонентов, которые на самом деле являются надмножествами или абстракциями поверх стандартных элементов HTML. Некоторые примеры включают элементы настраиваемых кнопок, которые могут принимать свойство, определяющее, должна ли эта кнопка быть основной или вторичной кнопкой, или, возможно, той, которая указывает, что она вызовет опасное действие, например удаление или удаление элемента из базы данных. Я по-прежнему хочу, чтобы моя кнопка имела все свойства кнопки в дополнение к реквизитам, которые я хочу к ней добавить.

Другой распространенный случай — в конечном итоге я создаю компонент, который позволяет мне одновременно определить метку и поле ввода. Я не хочу заново добавлять все свойства, которые <input />принимает элемент. Я хочу, чтобы мой пользовательский компонент вел себя точно так же, как поле ввода, но при этом принимал строку в качестве метки и автоматически подключал htmlForсвойство <label />в.id<input />

В JavaScript я могу просто {…props}передать любые реквизиты базовому элементу HTML. В TypeScript это может быть немного сложнее, где мне нужно явно определить, какие реквизиты будет принимать компонент. Хотя приятно иметь детальный контроль над точными типами, которые принимает мой компонент, может быть утомительно добавлять информацию о типе для каждого отдельного свойства вручную.

В некоторых сценариях мне нужен один адаптируемый компонент, например <div>, который меняет стили в соответствии с текущей темой. Например, возможно, я хочу определить, какие стили следует использовать в зависимости от того, включил ли пользователь вручную светлый или темный режим для пользовательского интерфейса. Я не хочу переопределять этот компонент для каждого отдельного элемента блока (например <section>, <article>, <aside>, и т. д.). Он должен быть способен представлять различные семантические элементы HTML, при этом TypeScript автоматически адаптируется к этим изменениям.

Есть несколько стратегий, которые мы можем использовать:

  • Для компонентов, в которых мы создаем абстракцию только для одного типа элементов, мы можем расширить свойства этого элемента.
  • Для компонентов, в которых мы хотим определить разные элементы, мы можем создать полиморфные компоненты. Полиморфный компонент — это компонент, предназначенный для отображения в виде различных элементов или компонентов HTML, сохраняя при этом те же свойства и поведение. Это позволяет нам указать свойство для определения типа отображаемого элемента. Полиморфные компоненты обеспечивают гибкость и возможность повторного использования без необходимости переопределять компонент. В качестве конкретного примера вы можете посмотреть реализацию полиморфного компонента в Radix.
Читайте также:  Chef или Puppet: сравнение инструментов управления конфигурацией с открытым исходным кодом

В этом уроке мы рассмотрим первую стратегию.

Зеркальное отображение и расширение свойств элемента HTML

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

const Button = (props) => {
  return <button className="button" {...props} />;
};

В TypeScript мы могли бы просто добавить то, что, как мы знаем, нам нужно. Например, мы знаем, что нам нужно, childrenесли мы хотим, чтобы наша пользовательская кнопка вела себя так же, как кнопка HTML:

const Button = (props: React.ComponentProps<'button'>) => {
  return <button className="button" {...props} />;
};

Вы можете себе представить, что добавление свойств по одному может оказаться немного утомительным. Вместо этого мы можем сообщить TypeScript, что мы хотим сопоставить те же реквизиты, которые он использовал бы для элемента <button>в React:

const Button = (props: React.ComponentProps<'button'>) => {
  return <button className="button" {...props} />;
};

Но у нас есть новая проблема. Или, скорее, у нас возникла проблема, которая также существовала в примере с JavaScript и которую мы проигнорировали. Если кто-то, использующий наш новый Buttonкомпонент, передаст реквизит className, он переопределит наш className. Мы могли бы (и мы добавим) добавить некоторый код для решения этой проблемы через мгновение, но я не хочу упускать возможность показать вам, как использовать служебный тип в TypeScript, чтобы сказать: «Я хочу использовать все реквизиты из HTML-кнопки, за исключением одного (или нескольких)»:

type ButtonProps = Omit<React.ComponentProps<'button'>, 'className'>;

const Button = (props: ButtonProps) => {
  return <button className="button" {...props} />;
};

Теперь TypeScript не позволит нам или кому-либо еще передать classNameсвойство в наш Buttonкомпонент. Если бы мы просто хотели расширить список классов за счет всего, что ему передается, мы могли бы сделать это несколькими разными способами. Мы могли бы просто добавить его в список:

type ButtonProps = React.ComponentProps<'button'>;

const Button = (props: ButtonProps) => {
  const className = 'button ' + props.className;

  return <button className={className.trim()} {...props} />;
};

Мне нравится использовать библиотеку clsx при работе с классами, поскольку она берет на себя большинство подобных задач:

import React from 'react';
import clsx from 'clsx';

type ButtonProps = React.ComponentProps<'button'>;

const Button = ({ className, ...props }: ButtonProps) => {
  return <button className={clsx('button', className)} {...props} />;
};
export default Button;

Мы узнали, как ограничить реквизиты, которые принимает компонент. Чтобы расширить реквизиты, мы можем использовать пересечение :

type ButtonProps = React.ComponentProps<'button'> & {
  variant?: 'primary' | 'secondary';
};

Теперь мы говорим, что Buttonпринимает все реквизиты, которые <button>принимает элемент, плюс еще один: variant. Этот реквизит будет отображаться вместе со всеми остальными реквизитами, которые мы унаследовали от HTMLButtonElement.

Теперь мы говорим, что Buttonпринимает все реквизи

Мы Buttonтакже можем добавить поддержку этого класса:

const Button = ({ variant, className, ...props }: ButtonProps) => {
  return (
    <button
      className={clsx(
        'button',
        variant === 'primary' && 'button-primary',
        variant === 'secondary' && 'button-secondary',
        className,
      )}
      {...props}
    />
  );
};

Теперь мы можем обновиться src/application.tsx, чтобы использовать наш новый компонент кнопки:

diff --git a/src/application.tsx b/src/application.tsx
index 978a61d..fc8a416 100644
--- a/src/application.tsx
+++ b/src/application.tsx
@@ -1,3 +1,4 @@
+import Button from './components/button';
 import useCount from './use-count';

 const Counter = () => {
@@ -8,15 +9,11 @@ const Counter = () => {
       <h1>Counter</h1>
       <p className="text-7xl">{count}</p>
       <div className="flex place-content-between w-full">
-        <button className="button" onClick={decrement}>
+        <Button onClick={decrement}>
           Decrement
-        </button>
-        <button className="button" onClick={reset}>
-          Reset
-        </button>
-        <button className="button" onClick={increment}>
-          Increment
-        </button>
+        </Button>
+        <Button onClick={reset}>Reset</Button>
+        <Button onClick={increment}>Increment</Button>
       </div>
       <div>
         <form
@@ -32,9 +29,9 @@ const Counter = () => {
         >
           <label htmlFor="set-count">Set Count</label>
           <input type="number" id="set-count" name="set-count" />
-          <button className="button-primary" type="submit">
+          <Button variant="primary" type="submit">
             Set
-          </button>
+          </Button>
         </form>
       </div>
     </main>

Создание составных компонентов

Другой распространенный компонент, который я обычно создаю для себя, — это компонент, который правильно связывает метку и элемент ввода с правильными атрибутами forи idатрибутами соответственно. Я устаю повторять это снова и снова:

<label htmlFor="set-count">Set Count</label>
<input type="number" id="set-count" name="set-count" />

Не расширяя реквизиты HTML-элемента, я мог бы постепенно добавлять реквизиты по мере необходимости:

type LabeledInputProps = {
  id?: string;
  label: string;
  value: string | number;
  type?: string;
  className?: string;
  onChange?: ChangeEventHandler<HTMLInputElement>;
};

Как мы видели на примере кнопки, мы можем провести ее рефакторинг аналогичным образом:

type LabeledInputProps = React.ComponentProps<'input'> & {
  label: string;
};

Помимо label, который мы передаем метке (ухх), которую нам часто нужно сгруппировать с нашими входными данными, мы вручную передаем реквизиты один за другим. Хотим ли мы добавить autofocus? Лучше добавьте еще одну опору. Лучше было бы сделать что-то вроде этого:

import { ComponentProps } from 'react';

type LabeledInputProps = ComponentProps<'input'> & {
  label: string;
};

const LabeledInput = ({ id, label, ...props }: LabeledInputProps) => {
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input {...props} id={id} readOnly={!props.onChange} />
    </>
  );
};

export default LabeledInput;

Мы можем заменить наш новый компонент в src/application.tsx:

<LabeledInput
  id="set-count"
  label="Set Count"
  type="number"
  onChange={(e) => setValue(e.target.valueAsNumber)}
  value={value}
/>

Мы можем извлечь то, с чем нам нужно работать, а затем просто передать все остальное компоненту <input />, а затем до конца наших дней просто притворяться, что это стандарт HTMLInputElement.

TypeScript это не волнует, поскольку HTMLElementон довольно гибок, поскольку DOM предшествует TypeScript. Он будет жаловаться только в том случае, если мы бросим туда что-то совершенно вопиющее.

Оцените статью
bestprogrammer.ru
Добавить комментарий