I'm going to post this answer anyways in case anyone else is interested or wants to understand the process behind this. You mentioned that it probably wouldn't get past code review, and I don't think there is any other solution that actually has this kind of behavior.
The core idea is to have the parameter default to another type. Typically, it'll be either void or never. Personally, I like to use never because it is the "empty" type.
Since the parameter will default to some type, you can just check for that type! If it's the default type, then you need to somehow force an error to appear where they call it, ideally with a helpful message.
function SomeComponent<T = never>(props: { /* regular props here */ } & (
[T] extends [never] ? { error: "generic parameter T is required" } : {}
)): JSX.Element;
function SomeComponent(props: { /* regular props here */ }) {
return <div></div>;
}
Breakdown of the shenanigans at play:
- Check if
T is the default (in this case never)¹
- If it is the default, then we need to make an error happen. In this example, I do it by requiring an extra property to be added. Obviously, the consumer shouldn't actually put the property there; it's just there to carry the more helpful error message ("generic parameter T is required") forward.
- If it is not the default, then don't do anything as they have passed the parameter. In this example, that means I have to intersect the regular props with
{} (effectively doing nothing).
- Because
props now has the complicated type { /* regular props here */} & (...), there's probably going to be some problems when trying to use it. To get around this, I moved everything into an overload so inside the body of the function, props still has the regular type { /* regular props here */ }. Of course, if you need to use T inside the function, then keep it as it is. Otherwise, I suggest doing the same.
Alright, so how does this work in practice?
const x = <SomeComponent />; // error!
const y = <SomeComponent<string> />; // okay
It seems to work as expected. However, if someone passes a generic type and that type happens to match the default, then you will still get an error:
const z = <SomeComponent<never> />; // still errors even though one was passed
Hopefully, you picked a default type that doesn't make sense to pass. Unfortunately, I don't think there's a way around this as there is no way to know if T used the default or not.
Playground
¹ The fact that I am using never is special in that it now requires the use of brackets ([T] extends [never]) to disable distribution. If you use something other than never, then T extends ... will probably work.
P.S. I found this related Q&A with some alternatives for you if you don't think this kind of solution can pass code review: TypeScript require generic parameter to be provided
<SomeComponent<MyType> />and it works.Tis the default, and then force an error: tsplay.dev/WzQE3m<MyType>part is omitted.