reactjs - ¿Cómo puedo abstraer el paso de propiedades repetidas de los hijos manteniendo los tipos como no opcionales?
Tengo un componente que se usa varias veces seguidas con algunas propiedades idénticas y otras únicas:
interface InsideComponentProps {
repeatedThing: string;
uniqueThing: string;
}
const InsideComponent: React.SFC<InsideComponentProps> = ({ repeatedThing, uniqueThing }) => (
<div>{repeatedThing} - {uniqueThing}</div>
);
const Example = () => (
<div>
<InsideComponent repeatedThing="foo" uniqueThing="1" />
<InsideComponent repeatedThing="foo" uniqueThing="2" />
<InsideComponent repeatedThing="foo" uniqueThing="3" />
</div>
);
Las propiedades duplicadas repeatedThing me molestan, así que estoy buscando una forma de eliminar esa redundancia. Una cosa que he hecho en aplicaciones que no son de TypeScript es introducir un componente contenedor que clona a todos los hijos, añadiendo las propiedades repetidas en el proceso:
interface OutsideComponentProps {
repeatedThing: string;
}
const OutsideComponent: React.SFC<OutsideComponentProps> = ({ repeatedThing, children }) => (
<div>
{React.Children.map(children, (c: React.ReactElement<any>) => (
React.cloneElement(c, { repeatedThing })
))}
</div>
);
const Example = () => (
<OutsideComponent repeatedThing="foo">
<InsideComponent uniqueThing="1" />
<InsideComponent uniqueThing="2" />
<InsideComponent uniqueThing="3" />
</OutsideComponent>
);
El código JavaScript resultante tiene el comportamiento que quiero, pero el compilador de TypeScript tiene errores porque no estoy pasando todas las propiedades requeridas al instanciar InsideComponent:
ERROR in [at-loader] ./src/index.tsx:27:26
TS2322: Type '{ uniqueThing: "1"; }' is not assignable to type 'IntrinsicAttributes & InsideComponentProps & { children?: ReactNode; }'.
Type '{ uniqueThing: "1"; }' is not assignable to type 'InsideComponentProps'.
Property 'repeatedThing' is missing in type '{ uniqueThing: "1"; }'.
La única solución que se me ha ocurrido es marcar la propiedad repeatedThing de los InsideComponent como opcional, pero eso no es ideal porque el valor es obligatorio.
¿Cómo puedo mantener la rigurosidad de asegurar que InsideComponent realmente reciba todas las propiedades mientras reduzco la duplicación de las propiedades en el sitio de llamada?
Estoy usando React 16.2.0 y TypeScript 2.6.2.
Respuesta #1
TypeScript verifica que asignes todas las propiedades requeridas al elemento de React. Dado que asignas las propiedades adicionales en el OutsideComponent, el compilador realmente no puede verificarlo.
Una opción sería especificar children como una función que toma propiedades adicionales como parámetro y las distribuye a InsideComponent. La sintaxis es un poco más enrevesada, pero es más segura en cuanto a tipos:
interface OutsideComponentProps {
repeatedThing: string;
children: (outerProps: OutsideComponentProps) => React.ReactElement<any>;
}
const OutsideComponent: React.SFC<OutsideComponentProps> = (o) => o.children(o);
const Example = () => (
<OutsideComponent repeatedThing="foo">{(o) =>
<div>
<InsideComponent uniqueThing="1" {...o} />
<InsideComponent uniqueThing="2" {...o} />
<InsideComponent uniqueThing="3" {...o} />
</div>
}</OutsideComponent>
);
Parece que
OutsideComponentes muy abstracto y por lo tanto reutilizable; ¿existe alguna manera de convertirlo en un componente muy genérico que tome todas sus propiedades y las proporcione como argumento, sin tener que definir unOutsideComponentPropspara cada caso?
Aunque puedes usar funciones genéricas como componentes, no puedes especificar los parámetros de tipo explícitamente, solo se pueden inferir. Esto es una desventaja, pero en última instancia se puede solucionar.
function GenericOutsideComponent<T>(props: { children: (o: T) => React.ReactElement<any> } & Partial<T>, context?: any): React.ReactElement<any> {
return props.children(props as any);
}
const Example = () => (
<GenericOutsideComponent repeatedThing="foo">{(o: InsideComponentProps) =>
<div>
<InsideComponent uniqueThing="1" {...o} />
<InsideComponent uniqueThing="2" {...o} />
<InsideComponent uniqueThing="3" {...o} />
</div>
}</GenericOutsideComponent>
);
Similar a tu solución original en JavaScript, existe el riesgo de que algunas propiedades requeridas de InsideComponent no estén especificadas ya que las propiedades de GenericOutsideComponent son Partial<T> (para permitir que solo repeatedThing sea especificado) y o es T porque de lo contrario el compilador considerará que repeatedThing no está especificado y lo requerirá en InsideComponent. Si establecer un valor ficticio en InsideComponent no es un problema, simplemente cambia la firma de children a (o: Partial<T>) => React.ReactElement<any> pero esto es menos que ideal.
Otra opción es ser explícito sobre qué propiedades están en GenericOutsideComponent usando un Pick:
function GenericOutsideComponent<T>(props: { children: (o: T) => React.ReactElement<any> } & T, context?: any): React.ReactElement<any> {
return props.children(props);
}
const Example = () => (
<GenericOutsideComponent repeatedThing="foo">{(o: Pick<InsideComponentProps, "repeatedThing">) =>
<div>
<InsideComponent uniqueThing="1" {...o} />
<InsideComponent uniqueThing="2" {...o} />
<InsideComponent uniqueThing="3" {...o} />
</div>
}</GenericOutsideComponent>
);
Respuesta #2
Soy nuevo en TypeScript, pero esto puede ser una alternativa:
// OutsideComponent.tsx
import * as React from "react";
const OutsideComponent: React.SFC<{ children?: React.ReactNode, [rest: string]: any }> = (props) => {
const { children, ...rest } = props;
return (
<div>
{React.Children.map(children, ((child, i) =>
React.cloneElement(child as React.ReactElement<any>, { key: i, ...rest }))
)}
</div>
)
};
type Sub<
O extends string,
D extends string
> = {[K in O]: (Record<D, never> & Record<string, K>)[K]}[O]
export type Omit<O, D extends keyof O> = Pick<O, Sub<keyof O, D>>
export default OutsideComponent;
(tipo Omit tomado de esta respuesta)
Entonces
import OutsideComponent, { Omit } from './OutsideComponent';
const Example = (): React.ReactElement<any> => {
const PartialInsideComponent: React.SFC<Omit<InsideComponentProps, 'repeatedThing'>> = InsideComponent;
return (
<OutsideComponent repeatedThing="foo">
<PartialInsideComponent uniqueThing="1" />
<PartialInsideComponent uniqueThing="2" />
<PartialInsideComponent uniqueThing="3" />
</OutsideComponent>
)
};
