Reputation: 2848
To avoid code duplication. We use some of the dynamic tag approaches. Basically, we have a component like this
type NavUtilDropdownProps = {
...
component?: keyof JSX.IntrinsicElements;
...
children: React.ReactElement;
};
const NavUtilDropdown: FC<NavUtilDropdownProps> = (props) => {
const ref = useRef<HTMLElement | null>(null);
const {
...
component: Component = 'div',
}
....
{/* the Component will be whatever string we pass in,such as div section or a */}
return <Component ref={ref} />
}
However, I'm getting a couple of TypeScript errors.
First. TS2590: Expression produces a union type that is too complex to represent.
I believe this is cause by the second error, which provides more information.
The second error is here.
Error:(163, 6) TS2322: Type '{ children: Element[]; ref: MutableRefObject<HTMLElement | null>; className: string; "data-testid": string; onMouseLeave: (event: MouseEvent<Element, MouseEvent>) => void; }' is not assignable to type 'SVGProps<SVGSymbolElement>'.
Types of property 'ref' are incompatible.
Type 'MutableRefObject<HTMLElement | null>' is not assignable to type 'LegacyRef<SVGSymbolElement> | undefined'.
Type 'MutableRefObject<HTMLElement | null>' is not assignable to type 'RefObject<SVGSymbolElement>'.
Types of property 'current' are incompatible.
Type 'HTMLElement | null' is not assignable to type 'SVGSymbolElement | null'.
Type 'HTMLElement' is missing the following properties from type 'SVGSymbolElement': ownerSVGElement, viewportElement, correspondingElement, correspondingUseElement, and 2 more.
Minimal example:
import React, { FC, useRef } from 'react'
type NavUtilDropdownProps = {
component?: keyof JSX.IntrinsicElements;
};
const NavUtilDropdown: FC<NavUtilDropdownProps> = ({component: Component}) => {
const ref = useRef<HTMLElement | SVGSymbolElement | null>(null);
if (!Component) return null
return <Component ref={ref} /> // type errors
}
See playground
Upvotes: 3
Views: 1534
Reputation: 33691
This involves a few parts, and it's not simple, but I'll try to break it down.
Note that this answer infers that you want to return a not-deprecated HTML React element, because:
In your original question details, the generic provided to
useRef
wasHTMLElement | null
using deprecated APIs in new/refactored code is an anti-pattern
assigning a
ref
to a standard component via props is not supported — for that you'd needforwardRef
)
Let's start with the props:
type NavUtilDropdownProps = {
component?: keyof JSX.IntrinsicElements;
};
The type keyof
JSX.IntrinsicElements
represents a union of many strings including deprecated element tag names, SVG element tag names, etc.
To get a union of strings representing not-deprecated HTML element tag names, you can use keyof
HTMLElementTagNameMap
.
To get a more exhaustive list (including SVG tag names), you can use something like this:
import { type ReactHTML, type ReactSVG } from "react"; type AnyHtmlOrSvgTagNameNotDeprecated = Exclude< keyof ReactHTML | keyof ReactSVG, keyof HTMLElementDeprecatedTagNameMap >;
Note that there are also MathML tags, but I'm not sure about React's support of those.
With that out of the way, let's move on to the described problem:
In the return statement return <Component ref={ref} />
(playground), the JSX markup is inferred by the compiler to be a union of all types of React elements associated to the string literals in the union keyof JSX.IntrinsicElements
. And because of that, the ref
attribute is inferred to be a mapped type representing a union of all of those elements' associated ref types. This mapped union is too complex for the compiler to represent (it's a current limitation of TypeScript's control flow analysis). That's the reason for the first error. The reason for the second error is that no specific element type can satisfy the union of all possible element types.
So what can be done about it? There are several solutions — let's explore:
As a simple solution, you can ignore the compiler diagnostic error by using a // @ts-expect-error
comment directive on the preceding line:
import { type ReactElement, useRef } from "react";
type NavUtilDropdownProps = {
tagName?: keyof HTMLElementTagNameMap;
};
const NavUtilDropdown = (
{ tagName: TagName = "div" }: NavUtilDropdownProps,
): ReactElement => {
const ref = useRef<HTMLElement>(null);
// @ts-expect-error
return <TagName ref={ref} />;
};
Suppressing compiler diagnostic errors is generally a bad idea, but there are times when you (the programmer) have more knowledge than the compiler, so tools like this directive (and type assertions, etc.) exist for these cases.
If you prefer never to use such directives under any circumstance, then you can assign your more narrow-typed variables to wider-annotated, but compatible types that the compiler will accept:
import { type ClassAttributes, type ReactElement, useRef } from "react";
type NavUtilDropdownProps = {
tagName?: keyof HTMLElementTagNameMap;
};
const NavUtilDropdown = (
{ tagName = "div" }: NavUtilDropdownProps,
): ReactElement => {
const ref = useRef<HTMLElement>(null);
const TagName: string = tagName;
const attributes: ClassAttributes<HTMLElement> = { ref };
return <TagName {...attributes} />;
};
Finally, if you don't like either of the previous approaches, you can skip the JSX markup inference problem and use createElement
directly, and TypeScript will have no inference trouble:
import { createElement, type ReactElement, useRef } from "react";
type NavUtilDropdownProps = {
tagName?: keyof HTMLElementTagNameMap;
};
const NavUtilDropdown = (
{ tagName = "div" }: NavUtilDropdownProps,
): ReactElement => {
const ref = useRef<HTMLElement>(null);
return createElement(tagName, { ref });
};
You can read about the differences between createElement
and JSX transforms at these pages:
Aside: You might have noticed that I didn't use null
in a union with HTMLElement
for the generic type provided to useRef
:
const ref = useRef<HTMLElement>(null);
//^? const ref: RefObject<HTMLElement>
That's because it's an overloaded function, and the return type when calling it with null
as the initial value is RefObject<T>
— which already includes null
in the union for the type of the current
property:
interface RefObject<T> {
readonly current: T | null;
}
Upvotes: 2
Reputation: 3806
Try this:
import React, { FC, useRef } from 'react'
type PropHtmlType = HTMLElement | null;
type NavUtilDropdownProps = {
component?: React.ComponentType<{ref: React.MutableRefObject<PropHtmlType> }>;
};
const NavUtilDropdown: FC<NavUtilDropdownProps> = ({component: Component}) => {
const ref = useRef<PropHtmlType>(null);
if (!Component) return null;
return <Component ref={ref} />
}
Upvotes: 1
Reputation: 186984
I don't believe that you can use JSX if to create an HTML tag from a string tag name. And yes, keyof JSX.IntrinsicElements
is a string, is just one of a few specific strings that are all the valid tag names.
But JSX is just some syntax sugar for React.createElement
<div foo="bar">baz</div>
Is equivalent to:
React.createElement('div', { foo: 'bar' }, 'baz')
React.createElement
takes three arguments. First is the component (function or class) or the tag name as a string (one of keyof JSX.IntrinsicElements
). Second is the props. And third is the children.
In your case that would look like this:
return React.createElement(Component, {ref}, null)
Upvotes: 0
Reputation: 276171
Based on your error, you need to add SVGSymbolElement
to the union.
type NavUtilDropdownProps = {
...
component?: keyof JSX.IntrinsicElements;
...
children: React.ReactElement;
};
const NavUtilDropdown: FC<NavUtilDropdownProps> = (props) => {
const ref = useRef<HTMLElement | SVGSymbolElement | null>(null);
const {
...
component: Component = 'div',
}
....
{/* the Component will be whatever string we pass in,such as div section or a */}
return <Component ref={ref} />
}
Upvotes: 0