'Force a component property to be an h2
I'm trying to define a props interface for a component using Typescript which requires the value passed for that property to be an instance of <h2>-<h6> (not an <h1> though).
I initially tried to do this, but it doesn't force the typing like I want:
type HeaderType = 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
interface OwnProps {
title?: React.ReactElement<HeaderType>;
}
This doesn't seem to work either (and I believe it would include h1 which I don't want):
interface OwnProps {
title?: React.ReactElement<HTMLHeadingElement>;
}
I understand I could throw a run-time error after inspecting the input in the component, but is there any way to restrict the tag type for a prop in React?
Solution 1:[1]
This is a tricky problem, primarily around trying to get a good developer experience.
My first thought was H2 to H6 components, since we can then restrict title the way you say you want to (which I don't think we can with "h2" to "h6" tags). That seemed really promising until the dev experience, which turned out to be really disappointing. (I've included it below just for reference.)
My second thought was that title could be a {tag: HeaderType; text: string;} object type, but that's no better from a devex point of view than what you had and what Mike suggested.
But React gets really good use of tuples, and the devex of making title a tuple seems decent:
Here's the implementation:
type HeaderType = "h2" | "h3" | "h4" | "h5" | "h6";
type Title = [tag: HeaderType, text: ReactNode];
interface OwnProps {
title?: Title;
}
Using it on a Section component:
const Section = ({ title, children }: PropsWithChildren<OwnProps>) => {
return <section>
{title && createElement(title[0], { children: title[1] })}
{children}
</section>;
};
The component solution was this:
If it were components we were trying to do this for, we could readily do it as:
type HeaderType =
| React.ReactElement<typeof ThisComponent>
| React.ReactElement<typeof ThatComponent>
| React.ReactElement<typeof TheOtherComponent>
// ...
So we could make H2 through H6 components. We could do it by just repeating ourselves:
const H2 = (props: ComponentProps<"h2">) => <h2 {...props}/>;
const H3 = (props: ComponentProps<"h3">) => <h3 {...props}/>;
// ....
...but even though there are only five h2...h6 tags, maybe we'll want to do this for other tags as well. So maybe a utility function:
const componentFor = <Tag extends keyof JSX.IntrinsicElements>(tag: Tag) =>
(props: ComponentProps<Tag>) => React.createElement(tag, props);
Then defining these (and perhaps others some other time) is quite simple:
const H2 = componentFor("h2");
const H3 = componentFor("h3");
const H4 = componentFor("h4");
const H5 = componentFor("h5");
const H6 = componentFor("h6");
Then our HeaderType is:
type HeaderType =
| React.ReactElement<typeof H2>
| React.ReactElement<typeof H3>
| React.ReactElement<typeof H4>
| React.ReactElement<typeof H5>
| React.ReactElement<typeof H6>;
But the devex isn't that great, the auto-complete just shows all components in scope:
Solution 2:[2]
The best way to address this is probably to go back to a component where you tell it what heading text to use through the title property, with a separate property for "which level of heading this is", e.g.
import * as React from "react";
interface CardProps {
title: string;
level: 2 | 3 | 4 | 5 | 6;
}
const Card = function(props: CardProps) {
const heading = React.createElement(`h${props.level}`, {
children: props.title,
});
return (
<div className="some-card-class">
{heading}
...etc...
</div>
);
};
// TS error:
const card1 = <Card title="this is the title" level={1} />;
// Not a TS error:
const card2 = <Card title="this is the title" level={2} />;
(Although of course in this example, code questions should be raised because I wrote this as a card, and cards shouldn't have a heading "level"; semantically, heading depth is relative to the document they're in, and a card is essentially its own (embedded) mini-document on a page, so you'd actually want to only use h1, making the need for a level property moot =)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | T.J. Crowder |
| Solution 2 |


