I want to make a Tabs
component that infers the possible values of the active
prop based on what its children have as name
props. This is how I tried to do it:
import React from 'react'
interface TabProps<N extends string> {
name: N
}
function Tab<N extends string>(props: TabProps<N>) {
return null
}
type TabsType<N extends string = string> =
| React.FunctionComponentElement<TabProps<N>>
| React.FunctionComponentElement<TabProps<N>>[]
interface TabsProps<Tabs extends TabsType> {
children: Tabs
active: Tabs extends TabsType<infer N> ? N : unknown
}
export function Tabs<Tabs extends TabsType>(props: TabsProps<Tabs>) {
return null
}
const test = (
{/* I want active to be inferred as type 'bla' | 'hey' */}
<Tabs active="blabla">
<Tab name="bla" />
<Tab name="hey" />
</Tabs>
)
The problem here seems to be that the Tabs
type parameter of the Tabs
component gets inferred as JSX.Element
instead of React.FunctionComponentElement<TabProps<N>>[]
which has the consequence that the default type string
gets used instead, making the invalid value blabla
go unnoticed by typescript.
Does this have a workaround? Is this a fundamental limitation of JSX? Is this something that needs a feature request? Is this a bug?
I don't think it can work the way you are imagining it, because TypeScript cannot infer that kind of type information just from the JSX children of a component. I'd be very curious if someone else has a solution that would make that work in any semblance you're trying for.
Your best bet is probably to abstract the children of <Tabs>
into an object, which you can infer strict (literal) type information for. Then, using generics, you can extract the explicit (literal) name
values and enforce it on the active
prop. <Tabs>
will simply map its data
array into rendered JSX, a very common pattern in React.
import React from 'react'
function Tabs<D extends readonly {name: string}[]>({data, active}: {data: D, active: D[number]["name"]}) {
return (
<div>
{data.map(ea => {
return <div className={active === ea.name ? 'active' : ''}>{ea.name}</div>;
})}
</div>
);
}
const tabData = [
{
name: 'bla'
},
{
name: 'hey'
}
] as const;
const test = (
<Tabs data={tabData} active="bla" />
)
Try it on TypeScript playground. Observe that if you try and change the active
prop to something other than "bla" or "hey" it will warn you.
Note the use of as const
when declaring tabData
. That has the benefit of allowing TypeScript to infer the explicit (literal) string value of the name
properties, even though they are nested inside of an array of objects. The downside is that you can't a priori declare the type of tabData
to be, e.g., an array of TabObject
s, because by definition the shape of tabData
must be inferred at the time of its literal declaration with as const
in order to get that juicy strict string value inference.
However, you do get a useful sort of "delayed" type checking because of the way Tabs
is defined: if the shape of data
is invalid, <Tabs>
will complain that it's not getting the expected shape for that prop.
Depending on your preferred React paradigm, you may dislike passing a data
prop to <Tabs>
and having it render its children based on that, instead of the composed approach you tried above. But by the nature of trying to infer types the way you are, I don't think it can be helped.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With