DEV Community

Dylan Paulus
Dylan Paulus

Posted on • Originally published at dylanpaulus.com

Generic Type Arguments in JSX Elements

Originally Posted: Here

Generic Type Arguments in JSX Elements

Typescript recently released generic type arguments for JSX in their 2.9 release. It's a mouthfull, but what does that mean for us? A common use-case I'm excited for is allowing consumers of libraries to extend a component's props. Using dynamic components we'll look at allowing our components to be extended even more.

What Are Generic Type Arguments?

As shown in the Typescript release notes, generic type arguments are a way to create components using Typescript's generics syntax. Below is a side-by-side comparison of the old way vs. using generic type arguments.

The Old Way:

// Notice color isn't defined as a prop, and will error out normally
function Div(props: { value: string }) {
    const { value, ...rest } = this.props;

    return <div {...rest} />
}

// Using spread, we can trick Typescript into ignoring that color will be a prop
// on Div
function App() {
    return <Div {...{ color: "red" }} />
}
Enter fullscreen mode Exit fullscreen mode

Generic Type Arguments:

// Notice our new generic on the component
function Div<T extends object>(props: { value: string } & T) {
    const { value, ...rest } = props as any; // spreading on generics not yet supported

    return <div {...rest} />
}

interface IAdditionalProps {
    color: string;
}

// We can tell typescript our Div element has additional properties!
function App() {
    // Generic Type Arguments!
    return <Div<IAdditionalProps> color="red" value="TEXT!!!" />
}
Enter fullscreen mode Exit fullscreen mode

And the same can be used with class components:

// Notice our new generic on the component
class Div<T extends object> extends React.Component<{ value: string } & T> {
    public render() {
        const { value, ...rest } = this.props as any;

        return <div {...rest} />
    }
}

interface IAdditionalProps {
    color: string;
}

// We can tell typescript our Div element has additional properties!
function App() {
    return <Div<IAdditionalProps> color="red" value="TEXT!!" />
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Elements

Let's say we have a MenuItem component that could be overloaded with either a Router link component, or a html a tag. One way we might write this...

interface IProps {
    tag: React.ReactNode;
    children: React.ReactNode;
}

function MenuItem({ tag, children, ...rest }: IProps) {
    const Tag: React.ReactType = tag || "a";

    return (
        <Tag {...rest}>
            {children}
        </Tag>
    );
}
Enter fullscreen mode Exit fullscreen mode

MenuItem works perfect fine as a component, but when it's time to add additional properties, Typescript will yell. For example, the a tag needs a href prop. We don't want to hardcode href, because we can inject any type of element through the tag prop (React Router, button, etc).

<MenuItem tag="a" href="http://google.com">Click Me!</MenuItem> // Error because href isn't defined in IProps!
<MenuItem tag={Link} to="/home">Home</MenuItem> // Error because to isn't defined in IProps!
Enter fullscreen mode Exit fullscreen mode

We can fix our errors using generic type arguments.

interface IProps {
  tag: React.ReactNode;
  children: React.ReactNode;
}

function MenuItem<T extends object>(props: IProps & T) {
  const { tag, children, ...rest } = props as any;
  const Tag: React.ReactType = tag || "a";

  return (
      <Tag {...rest}>
          {children}
      </Tag>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now the consumer of our MenuItem component can tell us what additional properties are needed!

<MenuItem<{ href: string }> tag="a" href="http://google.com">Click Me!</MenuItem> // Success!
<MenuItem<{ to: string }> tag={Link} to="/home">Home</MenuItem> // Success!
Enter fullscreen mode Exit fullscreen mode

Through generic type arguments for JSX, we are able to make our component more reusable. Users can extend components to allow additional props. Great!

Discussion (0)