I have two components with similar props, but there is a crucial difference. One component, called TabsWithState
takes only a single prop tabs
, which is an array of objects of the following shape:
interface ItemWithState {
name: string;
active: boolean;
}
interface WithStateProps {
tabs: ItemWithState[];
};
Another similar component, called TabsWithRouter
, requires the item shape to be different:
interface ItemWithRouter {
name: string;
path: string;
}
interface WithRouterProps {
tabs: ItemWithRouter[];
};
I am trying to create a generic component Tabs
, which would account for both scenarios. I want to be able to write a <Tabs />
component, where if a withRouter
prop is passed, the tabs
property must be of type ItemWithRouter[]
. But if no withRouter
prop is passed, it must of type ItemWithState[]
. Also, if withRouter
is passed, Tabs
should also accept an optional baseUrl
prop.
I tried creating a discriminating union type, like this:
type WithStateProps = {
withRouter?: never;
baseUrl?: never;
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: boolean;
baseUrl?: string;
tabs: ItemWithRouter[];
};
type TabsProps = WithStateProps | WithRouterProps;
In my generic Tabs
component, I want to render TabsWithRouter
if withRouter
is present, and TabsWithState
if withRouter
is not present:
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
I initially tried to define TabsWithRouter
and TabsWithState
as being function components that accept WithRouterProps
and WithStateProps
, respectively:
const TabsWithRouter: React.FC<WithRouterProps> = (props: WithRouterProps) => { ... }
const TabsWithState: React.FC<WithStateProps> = (props: WithStateProps) => { ... }
But I get the error Types of property 'withRouter' are incompatible. Type 'undefined' is not assignable to type 'boolean'.
, as can be seen in this ts playground
So I tried instead to type TabsWithRouter
and TabsWithState
as accepting TabsProps
as their props:
const TabsWithRouter: React.FC<TabsProps> = (props: TabsProps) => {
const { tabs } = props;
console.log(tabs[0].path)
return null
}
const TabsWithState: React.FC<TabsProps> = (props: TabsProps) => {
const { tabs } = props;
console.log(tabs[0].active)
return null
}
But in this case, trying to access tabs[x].path
or tabs[x].active
gives me the error Property 'active' does not exist on type 'ItemWithState | ItemWithRouter'. Property 'active' does not exist on type 'ItemWithRouter'
, as can be seen in this ts playground.
Interestingly, in both cases, when I actually try to use the component, the props are behaving correctly, as can be seen in some examples at the bottom of either ts playground.
I feel like I'm close, but I'm struggling to get these discriminating union types to behave in the way that I want so that typescript stops erroring. I've read many posts on here asking similar questions, but I can't seem to apply them to what is going wrong in my scenario.
As per request, here is my tsconfig.json:
{
"extends": "./tsconfig.paths.json",
"compilerOptions": {
"baseUrl": "src",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": [
"cypress",
"cypress-file-upload",
"jest"
],
"downlevelIteration": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
]
}
this ts playground shows that captain-yossarian's solution doesn't enforce the kinds of types I'm looking to enforce on the Tabs
component
How about something simple like this... giving TypeScript a hint using as WithRouterProps
or as WithStateProps
as appropriate?
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter !== undefined) {
return <TabsWithRouter {...props as WithRouterProps} />;
}
return <TabsWithState {...props as WithStateProps} />;
};