fairly simple question here. I'm trying to create a generic type like this:
export type RenderObject<TMeta = any> = {
meta?: TMeta;
render: (meta: TMeta) => JSX.Element;
}
And I'd like for the TMeta
type to be inferred without having to pass it into the RenderObject generic parameter when casting an object to this type, or having an extra function wrapper or class constructor to infer the TMeta
type. So like this:
const thing: RenderObject = {
meta: { foo: 'bar' },
render: (meta) => <i>{meta.foo}</i>,
}
However this just treats the TMeta
as any
in the render function instead of inferring the { foo: string }
type from the assignment.
Is what I'm trying to do possible? I need to avoid the extra function or class wrapper as this must remain highly performant and avoid adding any extra calls to the stack. Apologies if this has already been answered elsewhere, I couldn't seem to find this exact question anywhere but maybe my search was overlapping with too many other similar but different questions.
Thanks in advance!
No, this is not possible as stated.
The issues at microsoft/TypeScript#32794 and microsoft/TypeScript#30120 ask for a way to annotate a declared variable with a generic type, where the compiler should infer the generic type parameter instead of requiring manual specification. The latter issue was closed as a duplicate of microsoft/TypeScript#26242, a proposal to support "partial type parameter inference", which would deal with the more general issue of letting developers ask the compiler to infer type parameters in situations where currently the only solution is manual specification. As you've noticed, generic parameter defaults do not address this: while defaults (like <T = any>
) allow you to leave out type parameters, the compiler does not infer anything when you do; it merely substitutes the default for the omitted parameter.
If microsoft/TypeScript#26242 or microsoft/TypeScript#32794 ever gets implemented, it might be possible to write something like this:
// ⚠ NOT VALID TYPESCRIPT, DO NOT USE ⚠
const thing: RenderObject<infer> = {
meta: { foo: 'bar' },
render: (meta) => <i>{meta.foo}</i>,
};
But for now you cannot. The GitHub issue is open, but has been around for a while without any obvious recent movement. I don't know if it will matter one way or another, but if you feel strongly that this should be supported, you might want to go to that issue and give it a 👍, or describe your use case if you think it is particularly compelling compared to the ones already there.
Without such support, anything you do to get this sort of behavior will be a workaround.
The workaround I normally advocate is to use a helper function, since calling a generic function is one of the places where the compiler will actually infer type parameters for you:
const asRenderObject = <T,>(x: RenderObject<T>) => x;
const thing = asRenderObject({
meta: { foo: 'bar' },
render: (meta) => <i>{meta.foo}</i>,
})
/* const thing: RenderObject<{
foo: string;
}> */
This is fairly elegant, or at least arguably as elegant as RenderObject<infer>
. You get the type you wanted without redundant specification. The drawback is that at runtime, you have an extra function call. Only you can truly know if adding a call there would have any noticeable effect on the runtime performance of your code. I'm a little skeptical of that, since it implies that you are creating many thousands of these objects in a very short time... and if so, you probably want to look into refactoring so as to improve the runtime performance before you worry about TypeScript type inference. From here on out I will take it for granted that an extra function call is unacceptable.
At this point the only other workaround I can think of is to manually specify the types; either by annotating the variable declaration:
interface Thang { foo: string }
const thing: RenderObject<Thang> = {
meta: { foo: 'bar' },
render: (meta) => <i>{meta.foo}</i>,
};
or by letting the compiler infer the variable type, but annotating the render
callback parameter (since there's nowhere for the compiler to type it contextually):
interface Thang { foo: string }
const thing3 = {
meta: { foo: 'bar' },
render: (meta: Thang) => <i>{meta.foo}</i>,
};
But now there's no type parameter inference happening at all... this "workaround" is to avoid the issue entirely.
So, there you go. Until and unless microsoft/TypeScript#26242 or the like is implemented, there is no way to do what you're asking. Sorry!