This is a tricky one, and the only viable way I can think to do this is with a higher order component and function overloading.
Basically, we have to create a function that will itself return one type of component or the other depending on what argument its passed.
// Overload signature #1
function MakeInput(
type: "textArea"
): React.ForwardRefExoticComponent<
TextAreaProps & React.RefAttributes<HTMLTextAreaElement>
>;
// Overload signature #2
function MakeInput(
type: "input"
): React.ForwardRefExoticComponent<
InputProps & React.RefAttributes<HTMLInputElement>
>;
// Function declaration
function MakeInput(type: "textArea" | "input") {
if (type === "textArea") {
const ret = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
(props, ref) => {
return <TextArea {...props} ref={ref} />;
}
);
return ret;
} else {
const ret = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <Input {...props} ref={ref} />;
});
return ret;
}
}
Then, instantiate the component type you want to render by calling the higher order component function MakeInput() with the "type" of the component:
export default function App() {
const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
const inputRef = React.useRef<HTMLInputElement>(null);
const MyTextArea = MakeInput("textArea");
const MyInput = MakeInput("input");
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<MyTextArea value={"Foo"} ref={textAreaRef} />
<MyInput value={"Bar"} ref={inputRef} />
</div>
);
}
Now, this may feel "unsatisfying" because this is roughly equivalent to doing a conditional check here to see what type of component to render based on type, just abstracted away into a function. But, you can't render a magical <MyTextAreaOrInputComponent /> and get full type checking on both its props and ref attributes. And for that, you'll have to blame React itself, because the ref prop, like key and possibly some other props, are very very special and treated uniquely by React, which is exactly what necessitates React.forwardRef() in the first place.
But if you think about it, in practical terms you're still getting the prop type checking that you are looking for, it's just that you add an extra step of calling MakeInput() to determine the component type. So instead of writing this:
return <Component type="textArea" ref={textAreaRef} />
You're writing this:
const MyComponent = MakeInput("textArea");
return <MyComponent ref={textAreaRef} />
In both cases, you clearly must know the value of both type and ref at the time you are writing your code. The former case is impossible to get working (to my knowledge) because of the way React.forwardRef() works. But the latter case is possible, and gives you the exact same level of type checking, just with the extra step.
https://codesandbox.io/s/nostalgic-pare-pqmfu?file=/src/App.tsx
Note: play around with the sandbox above and see how even though <Input/> has an additional prop extraInputValue compared to <TextArea/>, the higher order component handles it gracefully. Also note that calling MakeInput() with either valid string value to create a component results in the expected and proper prop type checking.
Edit: Another illustration of how a "magic bullet" component vs. using a HOC are functionally identical in terms of type checking, since in your scenario you know both the type and what HTML element the ref should represent at pre-compile-time, you could literally just do this IIFE which contains the same amount of information:
return <div>
{(function(){
const C = MakeInput("textArea");
return <C value={"Baz"} ref={textAreaRef} />
})()}
</div>;