reactjs - ¿Es posible afirmar el tipo de una prop en el componente padre antes de que llegue a los hijos que usan la misma prop?
Tengo un componente NotFoundGuard que solo renderiza los hijos si la condición dada es verdadera. De lo contrario, renderiza un componente de reserva genérico para intentar evitar algo de redundancia en algo que es bastante común en nuestra base de código.
Estoy intentando averiguar si es posible asegurar a TypeScript que product no es null para cuando llegue a un componente hijo
<NotFound if={product != null}>
<ProductDisplay product={product} /> <- Type 'Product | null' not assignable to type 'Product'
</NotFound>
El componente NotFound se ve más o menos así
type Props = { if: boolean };
function NotFound(prop: Props): ReactNode {
if (!props.if) {
return children;
}
return <Fallback />;
}
Me gustaría evitar afirmar con el operador !. Actualizar el componente para que tome un render prop probablemente funcionaría bien, pero estaría más interesado en la "magia de tipos", incluso si es un poco truculenta.
Mi intento se veía así, lo cual, como era de esperar, no funcionó.
type Props = { if: boolean };
function dummyAssert(cond: boolean): asserts cond is true {}
function NotFound(prop: Props): ReactNode {
if (!props.if) {
dummyAssert(props.if);
return children;
}
return <Fallback />;
}
¿Es posible hacer que los typehints funcionen correctamente en un escenario como este?
<NotFound if={product != null}>
<ProductDisplay product={product} /> // No type error here about potential null prop
</NotFound>
Respuesta #1
Personalmente, dejaría de lado <NotFound /> por completo y simplificaría a:
{
product === null ? <Fallback /> : <ProductDisplay product={product} />
}
Pero dejemos eso de lado — y mantengamos <NotFound /> (imagina que tuviera más lógica de negocio, o que no quisiéramos exponer/repetir esta lógica en los componentes que contienen <NotFound />) — la solución correcta, en mi opinión, es el operador de afirmación de no nulo:
<NotFound if={product !== null}>
<ProductDisplay product={product!} />
</NotFound>
¿Por qué?
El meollo del asunto es que TS no sabe lo que if hace, en términos de tipo.
Así que tenemos que informarle, en el componente padre, sobre nuestra lógica personalizada (contenida en la directiva if) de una manera que TS entienda. Esto significa que la alternativa a usar el operador de no nulo es
<NotFound if={product !== null}>
{
product !== null && <ProductDisplay product={product} />
}
</NotFound>
lo cual, en mi opinión, es peor (tenemos que repetir el contenido de if dentro del marcado JSX).
Ahora demos un paso atrás y preguntémonos: “¿Qué es el operador de afirmación de no nulo? ¿Por qué existe y cuándo debería usarse?”
Es para los casos en los que TS no puede determinar correctamente la nulabilidad de una expresión. En este caso, sabemos con absoluta certeza que product no puede ser pasado como null a <ProductDisplay />, pero TS no puede determinar eso. 1
Entonces, el operador de afirmación de no nulo no es un error/truco/una forma de encubrir aquí, sino la solución correcta. Fue desarrollado precisamente para casos como el nuestro y debemos usarlo. 2
En tus propias palabras, es la "magia de tipeo" que has estado buscando.
1 - si tienes curiosidad sobre por qué TS no puede detectar lo que hace la directiva if, es porque TS no tiene conocimiento de cómo React interpreta JSX para crear elementos DOM y cuáles son las relaciones entre esos elementos DOM, una vez creados. En resumen, TS no sabe qué significa children en el contexto de un componente de React.
2 - el uso incorrecto del operador de no nulo sería suprimir una advertencia válida (en este ejemplo, si la posibilidad de pasar product como null a <ProductDisplay /> realmente existiera).
Respuesta #2
Ampliando la respuesta de tao, todavía es posible disfrutar de lo mejor de ambos mundos pasando los hijos condicionalmente en el elemento padre, en lugar de dentro del componente (como un hijo). De esa manera, el elemento padre procesa la verificación de tipo y notifica si hay algún conflicto, sin afirmar explícitamente que la prop no es nula (!). Para que esta solución funcione, el componente NotFoundGuard necesitaría ser refactorizado:
type Props = { children?: React.ReactNode };
function NotFoundGuard(props: Props): React.ReactNode {
if (props.children) {
return props.children;
}
return <Fallback />;
}
Uso :
<NotFoundGuard>
{product !== null && <ProductDisplay product={product} />}
</NotFoundGuard>
