On this fast tip, excerpted from Unleashing the Energy of TypeScript, Steve reveals you tips on how to use polymorphic elements in TypeScript.
In my article Extending the Properties of an HTML Aspect in TypeScript, I advised you that, over the course of constructing out a big utility, I have a tendency to finish up making a couple of wrappers round elements. Field
is a primitive wrapper across the primary block parts in HTML (corresponding to <div>
, <apart>
, <part>
, <article>
, <principal>
, <head>
, and so forth). However simply as we don’t wish to lose all of the semantic which means we get from these tags, we additionally don’t want a number of variations of Field
which are all principally the identical. What we’d love to do is use Field
but additionally have the ability to specify what it should be below the hood. A polymorphic part is a single adaptable part that may signify totally different semantic HTML parts, with TypeScript routinely adjusting to those modifications.
Right here’s an excessively simplified tackle a Field
ingredient 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">
Guardian field on the hill facet
<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 cross by any of the props, like we did with Button
and LabelledInputProps
above:
import { PropsWithChildren } from 'react';
kind BoxProps = PropsWithChildren< 'article' >;
const Field = ({ as, youngsters }: BoxProps) => {
const TagName = as || 'div';
return <TagName>{youngsters}</TagName>;
};
export default Field;
We refine as
to TagName
, which is a sound part title in JSX. That works as far a React is worried, however we additionally wish to get TypeScript to adapt accordingly to the ingredient we’re defining within the as
prop:
import { ComponentProps } from 'react';
kind BoxProps = ComponentProps<'div'> & 'article' ;
const Field = ({ as, youngsters }: BoxProps) => {
const TagName = as || 'div';
return <TagName>{youngsters}</TagName>;
};
export default Field;
I actually don’t even know if parts like <part>
have any properties {that a} <div>
doesn’t. Whereas I’m positive I might 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 have a look at the sort definition for ComponentPropsWithRef
, we see the next:
kind ComponentPropsWithRef<T extends ElementType> = T extends new (
props: infer P,
) => Element<any, any>
? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
: PropsWithRef<ComponentProps<T>>;
We are able to ignore all of these ternaries. We’re focused on ElementType
proper now:
kind BoxProps = ComponentPropsWithRef<'div'> & {
as: ElementType;
};
Okay, that’s fascinating, however what if we needed the sort argument we give to ComponentProps
to be the identical as … as
?
We might attempt one thing like this:
import { ComponentProps, ElementType } from 'react';
kind 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 ingredient kind we cross in with the as
prop.
We are able to 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 may see the ultimate consequence 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.