reactjs - ¿Cómo escribir un tipo genérico que incluya todas las props de un tipo genérico que no están en otro?
Estoy tratando de escribir un HOC genérico que tome un componente con propiedades TProps y un tipo TEntry que defina una estructura de objeto. Quiero que el HOC devuelva un componente que llame a un hook para obtener una instancia de objeto del tipo TEntry desde una fuente particular (en este caso, un CMS). Este componente resultante solo debería requerir que el consumidor proporcione las propiedades que no son rellenadas por esos datos del CMS.
Pensé que Exclude<TProps, TEntry> funcionaría para esto, pero aún termino con un componente que requiere todos los TProps. Esto es lo que he intentado (esto está bastante simplificado por simplicidad):
const withCmsData = <
TEntry,
TProps,
>(
Component: (props: TProps) => (JSX.Element | null),
fallback: TEntry,
) => {
return (props: Exclude<TProps, TEntry>) => {
const { entry } = useCmsEntries<TEntry>({ fallback });
return <Component {...entry} {...props} />;
};
};
...y aquí hay un ejemplo de cómo lo usarías para crear un nuevo componente con menos props:
type ArticlePageProps = {
title: string;
subTitle: string;
date: string;
}
const ArticlePage = ({ title, subTitle, date }: ArticlePageProps) => {
return (
<>
<h1>{title}</h1>
<div>{subTitle}</div>
<div>{date}</div>
</>
);
}
type ArticleFromCmsProps = {
title: string;
subTitle: string;
}
/*
This creates a new component that now only requires a data prop,
which is the one prop from ArticlePageProps that is
not provided by ArticleFromCmsProps:
*/
const fallback: ArticleFromCmsProps = getFallback();
const MyPageWrapper = withCmsData<ArticlePageProps, ArticleFromCmsProps>(ArticlePage, fallback);
const render = () => {
const data = new Date();
<MyPageWrapper date={date} />
}
En el ejemplo anterior, espero obtener un componente que solo requiera la prop date, pero en cambio, en tiempo de diseño en VSCode estoy recibiendo advertencias de que el componente todavía requiere las 3 props de ArticlePageProps -- <MyPageWrapper date={date} /> aparece con el mensaje de error que indica que faltan las propiedades title y subtitle
¿Hay una manera de obtener el HOC genérico que quiero? ¿Uno que devuelva un componente que solo requiera propiedades no especificadas por el parámetro de tipo TEntry?
Respuesta #1
He incluido algunas utilidades de tipos para reducir la repetición, pero la idea principal es que aceptes el objeto de reserva y el componente que deseas renderizar, infiriendo sus tipos usando genéricos para crear la firma del componente abstraído que utiliza la utilidad Omit<Type, Keys> para crear las props.
Debido a problemas en la forma en que TS maneja la bivarianza de los parámetros de las funciones, el tipo
PropsdeFunctionComponentnecesita ser bastante permisivo para evitar tener que sobrescribir los tipos de componente y de propiedad usando aserciones en el cuerpo del HOF.
Campo de Juegos TS
import {
default as React,
type ReactElement,
} from 'react';
// You didn't show this type in your question, so I made a best-effort guess:
declare function useCmsEntries <T>(param: { fallback: T; }): { entry: T };
type FunctionComponent<Props extends object = any> = (props: Props) => ReactElement | null;
type ComponentProps<Component extends FunctionComponent> = Parameters<Component>[0];
type ComponentWithExcludedProps<
Props extends object,
Component extends FunctionComponent<Props>,
> = FunctionComponent<Omit<ComponentProps<Component>, keyof Props>>;
type ArticlePageProps = {
date: string;
subTitle: string;
title: string;
};
function createComponentWithoutFallbackProps<
IncludedProps extends object,
C extends FunctionComponent,
>(
fallback: IncludedProps,
Component: C,
): ComponentWithExcludedProps<IncludedProps, C> {
return (props) => {
const { entry } = useCmsEntries<IncludedProps>({ fallback });
// Make sure you spread these in the desired order: props in the latter will overwrite the former
const all: ComponentProps<C> = {...entry, ...props};
return <Component {...all} />;
};
}
declare const ArticlePage: (props: ArticlePageProps) => ReactElement;
type ArticleFromCmsProps = Pick<ArticlePageProps, 'subTitle' | 'title'>;
declare const fallback: ArticleFromCmsProps;
const MyPageWrapper = createComponentWithoutFallbackProps(fallback, ArticlePage);
type MyPageWrapperProps = Parameters<typeof MyPageWrapper>[0]; // { date: string; } 👍
const render = () => {
const date = new Date().toISOString();
<MyPageWrapper date={date} /> // ok
};
