Как создать полиморфных компонентов в TypeScript? На чтение 4 мин Просмотров 50 Опубликовано 16.10.2023
В своей статье «Расширение свойств HTML-элемента в TypeScript» я рассказал вам, что в процессе создания большого приложения я склонен создавать несколько оберток вокруг компонентов. Box— это примитивная оболочка вокруг основных блочных элементов HTML (таких как <div>, <aside>, <section>, <article>, <main>, <head>и т. д.). Но так же, как мы не хотим потерять все семантическое значение, которое мы получаем от этих тегов, нам также не нужны многочисленные их варианты, Boxкоторые по сути одинаковы. Нам бы хотелось не только использовать, Boxно и иметь возможность указать, что должно быть внутри. Полиморфный компонент — это единый адаптируемый компонент, который может представлять различные семантические элементы HTML, при этом TypeScript автоматически адаптируется к этим изменениям.
Вот слишком упрощенный взгляд на Boxэлемент, вдохновленный Styled Components.
А вот пример компонента Boxиз Paste, дизайн-системы Twilio :
<Box as="article" backgroundColor="colorBackgroundBody" padding="space60">
Parent box on the hill side
<Box
backgroundColor="colorBackgroundSuccessWeakest"
display="inline-block"
padding="space40"
>
nested box 1 made out of ticky tacky
</Box>
</Box>
Вот простая реализация, в которой нет прохода через какие-либо реквизиты, как мы делали в предыдущем Buttonпримере LabelledInputProps:
import { PropsWithChildren } from 'react';
type BoxProps = PropsWithChildren<{
as: 'div' | 'section' | 'article' | 'p';
}>;
const Box = ({ as, children }: BoxProps) => {
const TagName = as || 'div';
return <TagName>{children}</TagName>;
};
export default Box;
Мы уточняем asдо TagName, которое является допустимым именем компонента в JSX. Это работает в отношении React, но мы также хотим, чтобы TypeScript адаптировался соответствующим образом к элементу, который мы определяем в свойстве as:
import { ComponentProps } from 'react';
type BoxProps = ComponentProps<'div'> & {
as: 'div' | 'section' | 'article' | 'p';
};
const Box = ({ as, children }: BoxProps) => {
const TagName = as || 'div';
return <TagName>{children}</TagName>;
};
export default Box;
Честно говоря, я даже не знаю, <section> есть ли у таких элементов какие-либо свойства, которых <div> нет. Хотя я уверен, что смогу это найти, никто из нас не доволен этой реализацией.
Но что это ’div’ там передается и как это работает? Если мы посмотрим на определение типа для ComponentPropsWithRef, мы увидим следующее: type ComponentPropsWithRef<T extends ElementType> = T extends new (
props: infer P,
) => Component<any, any>
? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
: PropsWithRef<ComponentProps<T>>;
Мы можем игнорировать все эти тройки. ElementTypeНас сейчас интересует :
type BoxProps = ComponentPropsWithRef<'div'> & {
as: ElementType;
};
Хорошо, это интересно, но что, если бы мы хотели, чтобы аргумент типа, который мы передаем, ComponentPropsбыл таким же, как … as?
Мы могли бы попробовать что-то вроде этого:
import { ComponentProps, ElementType } from 'react';
type BoxProps<E extends ElementType> = Omit<ComponentProps<E>, 'as'> & {
as?: E;
};
const Box = <E extends ElementType = 'div'>({ as, ...props }: BoxProps<E>) => {
const TagName = as || 'div';
return <TagName {...props} />;
};
export default Box;
Теперь Boxкомпонент будет адаптироваться к любому типу элемента, который мы передаем с помощью asсвойства.
Теперь мы можем использовать наш Boxкомпонент везде, где в противном случае мы могли бы использовать <div>:
<Box as="section" className="flex place-content-between w-full">
<Button className="button" onClick={decrement}>
Decrement
</Button>
<Button onClick={reset}>Reset</Button>
<Button onClick={increment}>Increment</Button>
</Box>