34

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>
    );
}
Sarun UK
  • 6,210
  • 7
  • 23
  • 48
callmetwan
  • 1,205
  • 2
  • 14
  • 31
  • Related [When to use JSX.Element vs ReactNode vs ReactElement?](https://stackoverflow.com/questions/58123398/when-to-use-jsx-element-vs-reactnode-vs-reactelement) – Liam Dec 01 '22 at 08:17

8 Answers8

25

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>
  );
}
Karol Majewski
  • 23,596
  • 8
  • 44
  • 53
  • best solution if your root element should be an intrinsic type - gives auto-completion and everything. – flq Dec 15 '21 at 07:47
17

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) => {}
Zico Deng
  • 645
  • 5
  • 14
  • 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! – callmetwan Jun 20 '19 at 15:30
4

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} />;
}
Sam Henderson
  • 101
  • 10
  • 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! – firxworx Nov 07 '22 at 22:41
3

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.

Eliav Louski
  • 3,593
  • 2
  • 28
  • 52
  • Noob here trying to understand...so e.g. that should be `type divType = JSX.IntrinsicElements["div"]`? Is that correct? – wamster Jun 25 '22 at 21:42
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]),
};
VyvIT
  • 1,048
  • 10
  • 13
0

You can use ReactElement:

import type {ReactElement} from 'react';
const HelloWorld = function HelloWorld({ prop: string }): ReactElement {
    return <div>{prop}</div>;
}
Saiansh Singh
  • 583
  • 5
  • 16
-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); 
}
Robert Lemiesz
  • 1,026
  • 2
  • 17
  • 29
  • 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. – callmetwan Jun 20 '19 at 15:26
-6

for me I added // @ts-ignore above the line that complained and it works

  • The whole point of TypeScript is that it allows you to have guarantees about how your code works. Ignoring type errors is like ignoring a check engine light in a car. It may work, or it may blow up. This is not a solution. – callmetwan Sep 21 '22 at 14:45
  • Or you could just not use typescript and the problem goes away – Aaron Dec 27 '22 at 09:51