41

I have a component that I want to default to being rendered as an h2. I'd like the consumer to be able to specify a different element if they desire. The code below results in the error:

TS2604 - JSX element type 'ElementType' does not have any construct or call signatures

I think I understand why it fails, TS is expecting to render a React node. For clarity, React is able to render elements referenced as strings as long as the variable begins with a capital letter (this being a JSX requirement). I've done this before successfully in vanilla JS + React, I just don't know how to satisfy TypeScript.

How can I get TypeScript to render this without resorting to elementType?: any

import React, {ReactNode} from 'react'

interface Props {
    children: ReactNode;
    elementType?: string;
}

export default function ({children, elementType: ElementType = 'h2'}: Props): JSX.Element {
    return (
        <ElementType>{children}</ElementType>
    );
}
1

7 Answers 7

30

Use keyof JSX.IntrinsicElements:

import * as React from 'react'

interface Props {
  children: React.ReactNode;
  elementType?: keyof JSX.IntrinsicElements;
}

export default function ({ children, elementType: ElementType = 'h2' }: Props): JSX.Element {
  return (
    <ElementType>{children}</ElementType>
  );
}
Sign up to request clarification or add additional context in comments.

1 Comment

best solution if your root element should be an intrinsic type - gives auto-completion and everything.
21

First, a bit about JSX. It is just a syntactic sugar for React.createElement, which is a JavaScript expression.

With this knowledge in mind, now let's take a look at why TypeScript complains. You define elementType as string, however, when you actually use it, it becomes a JavaScript expression. string type of course doesn't have any construct or call signature.

Now we know the root cause. In React, there is a type called FunctionComponent. As you can guess, it is a function expression, which is what we want. So you can define elementType as string | FunctionComponent. This should make TypeScript happy :)

FYI: the recommended way to define prop typing is by doing this:

const MyComponent: FunctionComponent<Props> = (props) => {}

1 Comment

This worked like a charm! Thanks for sharing the recommended way to define prop typing, I'm still learning the ins and outs of TS!
7

I was making a <Text> component and wanted to narrow the set of possible HTML tags that the developer could pass in to just "text based" elements like span, p, h1, etc.

So I had something like:


// Narrow the tags to p, span, h1, etc
type AS = Extract<keyof JSX.IntrinsicElements, 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5'>;

interface Props {
  children: React.ReactNode;
  as: AS;
  // ... other props here
}

export function Text(props: Props) {  

  // Render the text with the appropriate HTML tag.
  return (
    <HTMLTag as={props.as}>
      {props.children}
    </HTMLTag>
  );
}

interface HTMLTagProps extends HTMLAttributes<HTMLOrSVGElement> {
  as: AS;
}

function HTMLTag({ as: As, ...otherProps }: HTMLTagProps) {
  return <As {...otherProps} />;
}

1 Comment

Bingo! I wanted to restrict what was an overly permissive prop of type React.ElementType in a refactor pass because it should only accept headings. Using the generic of React.ElementType wasn't right... A quick search brought me to your answer and its exactly what I needed. Thanks for your answer!
4

if you just want the type of any jsx element you can use

type jsxType = JSX.IntrinsicElements[keyof JSX.IntrinsicElements]

this will accept any jsx element.

1 Comment

Noob here trying to understand...so e.g. that should be type divType = JSX.IntrinsicElements["div"]? Is that correct?
1

What worked for me given the component is defined like this:

interface LabelProps {
        ...
        tag?: React.ElementType | string;
    }

const Label: VFC<LabelProps> = ({
       ...other props...
        tag: Element = 'span',
    }) => (
        <Element>
            {children}
        </Element>
    );

and prop types:

Label.propTypes = {
    ...
    tag: PropTypes.oneOfType([PropTypes.elementType, PropTypes.string]),
};

Comments

1

You can use ReactElement:

import type {ReactElement} from 'react';
const HelloWorld = function HelloWorld({ prop: string }): ReactElement {
    return <div>{prop}</div>;
}

Comments

-2

That is not going to work, you need to use React.createElement(). https://reactjs.org/docs/react-api.html#createelement

Something like this

  import React, {ReactNode} from 'react'

interface Props {
    children: ReactNode,
    elementType?: string,
}

export default function ({children, elementType: ElementType = 'h2'}: Props): JSX.Element {

    return React.createElement(elementType, {}, children); 
}

1 Comment

With respect, this does work. What I posted is just syntactic sugar for your solution. This isn't an issue with React, it is an issue with TS.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.