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 was HTMLElement | 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 need forwardRef)
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:
TS Playground
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:
TS Playground
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:
TS Playground
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:
TS Playground
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;
}
Elementas the type (see developer.mozilla.org/en-US/docs/Web/API/Element), that should coverHTMLElementandSVGElement.