reactjs - Props de React - dificultades con la discriminación de tipos de unión
Tengo dos componentes con propiedades similares, pero hay una diferencia crucial. Un componente, llamado TabsWithState, recibe solo una propiedad tabs, que es un arreglo de objetos con la siguiente forma:
interface ItemWithState {
name: string;
active: boolean;
}
interface WithStateProps {
tabs: ItemWithState[];
};
Otro componente similar, llamado TabsWithRouter, requiere que la forma del elemento sea diferente:
interface ItemWithRouter {
name: string;
path: string;
}
interface WithRouterProps {
tabs: ItemWithRouter[];
};
Estoy intentando crear un componente genérico Tabs, que contemple ambos escenarios. Quiero poder escribir un componente <Tabs />, donde si se pasa una prop withRouter, la propiedad tabs debe ser del tipo ItemWithRouter[]. Pero si no se pasa la prop withRouter, debe ser del tipo ItemWithState[]. Además, si se pasa withRouter, Tabs también debería aceptar una prop opcional baseUrl.
Intenté crear un tipo de unión discriminante, así:
type WithStateProps = {
withRouter?: never;
baseUrl?: never;
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: boolean;
baseUrl?: string;
tabs: ItemWithRouter[];
};
type TabsProps = WithStateProps | WithRouterProps;
En mi componente genérico Tabs, quiero renderizar TabsWithRouter si withRouter está presente, y TabsWithState si withRouter no está presente:
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
Inicialmente intenté definir TabsWithRouter y TabsWithState como componentes de función que aceptan WithRouterProps y WithStateProps, respectivamente:
const TabsWithRouter: React.FC<WithRouterProps> = (props: WithRouterProps) => { ... }
const TabsWithState: React.FC<WithStateProps> = (props: WithStateProps) => { ... }
Pero obtengo el error Tipos de la propiedad 'withRouter' son incompatibles. El tipo 'undefined' no se puede asignar al tipo 'boolean'., como se puede ver en este playground de ts
Así que intenté en su lugar escribir TabsWithRouter y TabsWithState aceptando TabsProps como sus 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
}
Pero en este caso, intentar acceder a tabs[x].path o tabs[x].active me da el error Property 'active' does not exist on type 'ItemWithState | ItemWithRouter'. Property 'active' does not exist on type 'ItemWithRouter', como se puede ver en este playground de ts.
Curiosamente, en ambos casos, cuando realmente intento usar el componente, las props se comportan correctamente, como se puede ver en algunos ejemplos al final de cualquier playground de ts.
Siento que estoy cerca, pero me está costando hacer que estos tipos de unión discriminante se comporten de la manera que quiero para que TypeScript deje de dar errores. He leído muchas publicaciones aquí haciendo preguntas similares, pero no parece que pueda aplicarlas a lo que está saliendo mal en mi escenario.
Editar :
Según lo solicitado, aquí está mi 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"
]
}
Editar2
este playground de ts muestra que la solución de captain-yossarian no aplica los tipos que estoy buscando aplicar en el componente Tabs
Respuesta #1
¿Qué tal algo simple como esto... dar a TypeScript una pista usando as WithRouterProps o as WithStateProps, según corresponda?
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter !== undefined) {
return <TabsWithRouter {...props as WithRouterProps} />;
}
return <TabsWithState {...props as WithStateProps} />;
};
Respuesta #2
El control del flujo de condiciones con alias está disponible únicamente desde [email . Por lo tanto, usar la variable desestructurada withRouter no aporta ninguna información de tipo cuando se utiliza dentro de una declaración if antes de [email .
Entonces hay un problema sutil en tu código. Tu propiedad withRouter tiene tipo boolean y aquí:
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter) { // withRouter === true
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
Estás restringiendo su tipo a true, no a boolean (true | false).
Otro problema es que los tipos opcionales siempre introducen el valor undefined en la unión. Por lo tanto, tienes que verificar explícitamente tal caso. Esta verificación simultáneamente comprueba el valor explícito 'undefined' o el caso cuando !('withRouter' in props):
const Tabs = (props: TabsProps) => {
if (props.withRouter === undefined) {
return <TabsWithState {...props} />;
}
return <TabsWithRouter {...props} />;
};
enlace del parque
Desde [email puedes usar withRouter desestructurado como condición alias:
const Tabs = (props: TabsProps) => {
const { withRouter } = props
if (withRouter === undefined) {
return <TabsWithState {...props} />;
}
return <TabsWithRouter {...props} />;
};
verificación del patio de juegos
