On this fast tip, excerpted from Unleashing the Energy of TypeScript, Steve reveals you methods to use polymorphic elements in TypeScript.
In my article Extending the Properties of an HTML Aspect in TypeScript, I instructed you that, over the course of constructing out a big software, I have a tendency to finish up making just a few wrappers round elements. Field
is a primitive wrapper across the fundamental block parts in HTML (resembling <div>
, <apart>
, <part>
, <article>
, <most important>
, <head>
, and so forth). However simply as we don’t need to lose all of the semantic that means we get from these tags, we additionally don’t want a number of variations of Field
which are all mainly the identical. What we’d love to do is use Field
but in addition be capable of specify what it should be below the hood. A polymorphic part is a single adaptable part that may symbolize totally different semantic HTML parts, with TypeScript routinely adjusting to those adjustments.
Right here’s a very simplified tackle a Field
component impressed by Styled Parts.
And right here’s an instance of a Field
part from Paste, Twilio’s design system:
<Field as="article" backgroundColor="colorBackgroundBody" padding="space60">
Dad or mum field on the hill aspect
<Field
backgroundColor="colorBackgroundSuccessWeakest"
show="inline-block"
padding="space40"
>
nested field 1 made out of ticky cheesy
</Field>
</Field>
Right here’s a easy implementation that doesn’t have any go by way of any of the props, like we did with Button
and LabelledInputProps
above:
import { PropsWithChildren } from 'react';
sort BoxProps = PropsWithChildren< 'part' >;
const Field = ({ as, kids }: BoxProps) => {
const TagName = as || 'div';
return <TagName>{kids}</TagName>;
};
export default Field;
We refine as
to TagName
, which is a legitimate part identify in JSX. That works as far a React is worried, however we additionally need to get TypeScript to adapt accordingly to the component we’re defining within the as
prop:
import { ComponentProps } from 'react';
sort BoxProps = ComponentProps<'div'> & 'part' ;
const Field = ({ as, kids }: BoxProps) => {
const TagName = as || 'div';
return <TagName>{kids}</TagName>;
};
export default Field;
I truthfully don’t even know if parts like <part>
have any properties {that a} <div>
doesn’t. Whereas I’m positive I may look it up, none of us be ok with this implementation.
However what’s that 'div'
being handed in there and the way does it work? If we take a look at the sort definition for ComponentPropsWithRef
, we see the next:
sort ComponentPropsWithRef<T extends ElementType> = T extends new (
props: infer P,
) => Element<any, any>
? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
: PropsWithRef<ComponentProps<T>>;
We will ignore all of these ternaries. We’re considering ElementType
proper now:
sort BoxProps = ComponentPropsWithRef<'div'> & {
as: ElementType;
};
Okay, that’s fascinating, however what if we wished the sort argument we give to ComponentProps
to be the identical as … as
?
We may strive one thing like this:
import { ComponentProps, ElementType } from 'react';
sort BoxProps<E extends ElementType> = Omit<ComponentProps<E>, 'as'> & {
as?: E;
};
const Field = <E extends ElementType="div">({ as, ...props }: BoxProps<E>) => {
const TagName = as || 'div';
return <TagName {...props} />;
};
export default Field;
Now, a Field
part will adapt to no matter component sort we go in with the as
prop.
We will now use our Field
part wherever we would in any other case use a <div>
:
<Field as="part" className="flex place-content-between w-full">
<Button className="button" onClick={decrement}>
Decrement
</Button>
<Button onClick={reset}>Reset</Button>
<Button onClick={increment}>Increment</Button>
</Field>
You’ll be able to see the ultimate outcome on the polymorphic
department of the GitHub repo for this tutorial.
This text is excerpted from Unleashing the Energy of TypeScript, accessible on SitePoint Premium and from e book retailers.