Diego  Elizondo

Diego Elizondo

1654632000

Componentes Polimórficos Fuertemente Tipados Con React Y TypeScript

En esta guía detallada (y explicativa), discutiré cómo construir componentes React polimórficos fuertemente tipados con TypeScript.

Como puede ver, esto es bastante largo, así que siéntase libre de saltearlo. Si desea seguir, marque el repositorio de código oficial en mi GitHub como referencia.

Ejemplos del mundo real de componentes polimórficos

Existe una probabilidad distinta de cero de que ya haya usado un componente polimórfico. Las bibliotecas de componentes de código abierto normalmente implementan algún tipo de componente polimórfico.

Consideremos algunos con los que puede estar familiarizado: el asaccesorio Chakra UI y MUI component.

Accesorio de Chakra asUI

¿Cómo implementa Chakra UI accesorios polimórficos? La respuesta es exponer un asaccesorio. El asaccesorio se pasa a un componente para determinar qué elemento contenedor debería representar eventualmente.

Todo lo que necesita hacer para usar el asaccesorio es pasarlo al componente, que en este caso es Box:

<Box as='button' borderRadius='md' bg='tomato' color='white' px={4} h={8}>
  Button
</Box>

Ahora, el componente representará un buttonelemento.

Si cambiaste el asaccesorio a un h1:

<Box as="h1"> Hello </Box>

Ahora, el Boxcomponente representa un h1:

¡Eso es un componente polimórfico en el trabajo! Se puede representar en elementos completamente únicos, todo al pasar un solo accesorio.

apoyo de MUIcomponent

Similar a Chakra UI, MUI permite un accesorio polimórfico llamado component, que se implementa de manera similar: lo pasa a un componente y establece el elemento o componente personalizado que le gustaría representar.

Aquí hay un ejemplo de los documentos oficiales :

<List component="nav">
  <ListItem button>
    <ListItemText primary="Trash" />
  </ListItem>
</List>

Listse pasa un componente prop de nav; cuando esto se represente, generará un navelemento contenedor.

Otro usuario puede usar el mismo componente, pero no para la navegación; en su lugar, es posible que deseen generar una lista de tareas pendientes:

<List component="ol">
  ...
</List>

Y en este caso, Listrepresentará un elemento de lista ordenada ol.

¡Hablando de flexibilidad! Consulte un resumen de los casos de uso de los componentes polimórficos.

Como verá en las siguientes secciones de este artículo, los componentes polimórficos son poderosos. Además de aceptar un accesorio de un tipo de elemento, también pueden aceptar componentes personalizados como accesorios.

Esto se discutirá en una próxima sección de este artículo. Por ahora, ¡construyamos nuestro primer componente polimórfico!

Construcción de un componente polimórfico simple

Al contrario de lo que pueda pensar, construir su primer componente polimórfico es bastante sencillo. Aquí hay una implementación básica:

const MyComponent = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Tenga en cuenta aquí que el accesorio polimórfico ases similar a la interfaz de usuario de Chakra. Este es el accesorio que exponemos para controlar el elemento de representación del componente polimórfico.

En segundo lugar, tenga en cuenta que el asaccesorio no se representa directamente. Lo siguiente estaría mal:

const MyComponent = ({ as, children }) => {
  // wrong render below  
  return <as>{children}</as>;
};

Al renderizar un tipo de elemento en tiempo de ejecución , primero debe asignarlo a una variable en mayúsculas y luego renderizar la variable en mayúsculas.

Ahora puede continuar y usar este componente de la siguiente manera:

<MyComponent as="button">Hello Polymorphic!<MyComponent>
<MyComponent as="div">Hello Polymorphic!</MyComponent>
<MyComponent as="span">Hello Polymorphic!</MyComponent>
<MyComponent as="em">Hello Polymorphic!</MyComponent>

Tenga en cuenta que el asaccesorio diferente se pasa a los componentes representados arriba.

Problemas con esta implementación simple

La implementación de la sección anterior, aunque bastante estándar, tiene muchos inconvenientes. Exploremos algunos de estos.

1. El asaccesorio puede recibir elementos HTML no válidos

Actualmente, es posible que un usuario escriba lo siguiente:

<MyComponent as="emmanuel">Hello Wrong Element</MyComponent>

El asprop pasado aquí es emmanuel. Emmanuel es obviamente un elemento HTML incorrecto, pero el navegador también intenta representar este elemento.

Una experiencia de desarrollo ideal es mostrar algún tipo de error durante el desarrollo. Por ejemplo, un usuario puede cometer un error tipográfico simple, divven lugar de div, y no obtendría ninguna indicación de lo que está mal.

2. Se pueden pasar atributos incorrectos para elementos válidos

Considere el siguiente uso de componentes:

<MyComponent as="span" href="https://www.google.com">
   Hello Wrong Attribute
</MyComponent>

Un consumidor puede pasar un spanelemento a la aspropiedad y hreftambién una propiedad.

Esto es técnicamente inválido. Un spanelemento no toma (y no debería tomar) un hrefatributo. Esa es una sintaxis HTML no válida. Sin embargo, un consumidor del componente que hemos creado aún podría escribir esto y no recibir errores durante el desarrollo.

3. ¡Sin soporte de atributos!

Considere la implementación simple nuevamente:

const MyComponent = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Los únicos accesorios que acepta este componente son asy children, nada más. No hay compatibilidad con atributos ni siquiera aspara accesorios de elementos válidos, es decir, si asfuera un elemento de anclaje a, también deberíamos admitir pasar an hrefal componente.

<MyComponent as="a" href="...">A link </MyComponent>

Aunque hrefse pasa en el ejemplo anterior, la implementación del componente no recibe otros apoyos. Sólo asy childrense deconstruyen.

Sus pensamientos iniciales pueden ser seguir adelante y distribuir todos los demás apoyos pasados ​​al componente de la siguiente manera:

const MyComponent = ({ as, children, ...rest }) => {
  const Component = as || "span";

  return <Component {...rest}>{children}</Component>;
};

Esto parece una solución decente, pero ahora resalta el segundo problema mencionado anteriormente. Los atributos incorrectos ahora también se transmitirán al componente.

Considera lo siguiente:

<MyComponent as="span" href="https://www.google.com">
   Hello Wrong Attribute
</MyComponent>

Y tenga en cuenta el marcado renderizado eventual:

A spancon un hrefes HTML no válido.

¿Por qué es esto malo?

En resumen, los problemas actuales con nuestra implementación simple son insatisfactorios porque:

  • Proporciona una experiencia de desarrollador terrible
  • No es de tipo seguro. Los errores pueden (y lo harán) colarse

¿Cómo resolvemos estas preocupaciones? Para ser claros, no hay una varita mágica para agitar aquí. Sin embargo, aprovecharemos TypeScript para garantizar que construya componentes polimórficos fuertemente tipados.

Al finalizar, los desarrolladores que usen sus componentes evitarán los errores de tiempo de ejecución anteriores y, en su lugar, los detectarán durante el desarrollo o el tiempo de compilación, todo gracias a TypeScript.

Cómo usar TypeScript para construir componentes polimórficos fuertemente tipados en React

Si está leyendo esto, un requisito previo es que ya sepa algo de TypeScript, al menos los conceptos básicos. Si no tiene idea de qué es TypeScript, le recomiendo que lea este documento primero.

En esta sección, utilizaremos TypeScript para resolver los problemas antes mencionados y construir componentes polimórficos fuertemente tipados.

Los primeros dos requisitos con los que comenzaremos incluyen:

  • La aspropiedad no debe recibir cadenas de elementos HTML no válidas.
  • No se deben pasar atributos incorrectos para elementos válidos

En la siguiente sección, presentaremos los genéricos de TypeScript para hacer que nuestra solución sea más robusta, fácil de usar para desarrolladores y digna de producción.

Asegurarse de que la aspropiedad solo reciba cadenas de elementos HTML válidas

Esta es nuestra solución actual:

const MyComponent = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Para que las próximas secciones de esta guía sean prácticas, cambiaremos el nombre del componente de MyComponenta Texty supondremos que estamos construyendo un Textcomponente polimórfico.

const Text = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Ahora, con su conocimiento de los genéricos, se vuelve obvio que es mejor representar ascon un tipo genérico, es decir, un tipo de variable basado en lo que el usuario pase.

Sigamos adelante y demos el primer paso de la siguiente manera:

export const Text = <C>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Tenga en cuenta cómo se define el genérico Cy luego se transmite en la definición de tipo para el accesorio as.

Sin embargo, si escribió este código aparentemente perfecto, TypeScript gritará numerosos errores con más líneas rojas onduladas de lo que le gustaría.🤷‍♀️

Lo que está pasando aquí es una falla en la sintaxis de los genéricos en los .tsxarchivos. Hay dos formas de resolver esto.

1. Agregue una coma después de la declaración genérica

Esta es la sintaxis para declarar varios genéricos. Una vez que haga esto, el compilador de TypeScript entiende claramente su intención y los errores desaparecen.

// note the comma after "C" below 
export const Text = <C,>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

2. Restringir lo genérico

La segunda opción es restringir el genérico como mejor le parezca. Para empezar, puede usar el unknowntipo de la siguiente manera:

// note the extends keyword below 
export const Text = <C extends unknown>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Por ahora, me ceñiré a la segunda solución porque está más cerca de nuestra solución final. Sin embargo, en la mayoría de los casos, uso la sintaxis genérica múltiple y solo agrego una coma.

Sin embargo, con nuestra solución actual, obtenemos otro error de TypeScript:

El tipo de elemento JSX 'Componente' no tiene ninguna construcción ni firma de llamada. ts(2604)

Esto es similar al error que tuvimos cuando trabajamos con la echoLengthfunción. Al igual que acceder a la lengthpropiedad de un tipo de variable desconocido, se puede decir lo mismo aquí: tratar de representar cualquier tipo genérico como un componente React válido no tiene sentido.

Necesitamos restringir el genérico solo para que se ajuste al molde de un tipo de elemento React válido.

Para lograr esto, aprovecharemos el tipo React interno: React.ElementTypey nos aseguraremos de que el genérico esté restringido para adaptarse a ese tipo:

// look just after the extends keyword 
export const Text = <C extends React.ElementType>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Tenga en cuenta que si está utilizando una versión anterior de React, es posible que deba importar una versión más nueva de React de la siguiente manera:

import React from 'react'

¡Con esto, no tenemos más errores!

Ahora, si continúa y usa este componente de la siguiente manera, funcionará bien:

<Text as="div">Hello Text world</Text>

Sin embargo, si pasa una propiedad no válida as, ahora obtendrá un error de TypeScript apropiado. Considere el siguiente ejemplo:

<Text as="emmanuel">Hello Text world</Text>

Y el error arrojado:

El tipo '”emmanuel”' no se puede asignar al tipo 'ElementType | indefinido'.


¡Esto es excelente! Ahora tenemos una solución que no acepta galimatías para el asaccesorio y también evitará errores tipográficos desagradables, por ejemplo, divven lugar de div.

¡Esta es una experiencia de desarrollador mucho mejor!

Manejo de atributos de componentes válidos con genéricos de TypeScript

Al resolver este segundo caso de uso, llegará a apreciar cuán poderosos son realmente los genéricos. Primero, entendamos lo que estamos tratando de lograr aquí.

Una vez que recibimos un astipo genérico, queremos asegurarnos de que los accesorios restantes pasados ​​a nuestro componente sean relevantes, según el asaccesorio.

Entonces, por ejemplo, si un usuario pasó un asaccesorio de img, ¡ hrefquerríamos ser igualmente un accesorio válido!

Para darle una idea de cómo lograríamos esto, eche un vistazo al estado actual de nuestra solución:

export const Text = <C extends React.ElementType>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

La propiedad de este componente ahora está representada por el tipo de objeto:

{
  as?: C;
  children: React.ReactNode;
}

En pseudocódigo, lo que nos gustaría sería lo siguiente:

{
  as?: C;
  children: React.ReactNode;
} & {
  ...otherValidPropsBasedOnTheValueOfAs
}

Este requisito es suficiente para dejar a uno aferrado a un clavo ardiendo. No podemos escribir una función que determine los tipos apropiados en función del valor de as, y no es inteligente enumerar un tipo de unión manualmente.

Bueno, ¿qué pasaría si hubiera un tipo proporcionado Reactque actuara como una "función" que devolverá tipos de elementos válidos en función de lo que le pases?

Antes de presentar la solución, refactoricemos un poco. Saquemos los accesorios del componente en un tipo separado:

//  See TextProps pulled out below 
type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} 

export const Text = <C extends React.ElementType>({
  as,
  children,
}: TextProps<C>) => { //  see TextProps used 
  const Component = as || "span";
  return <Component>{children}</Component>;
};

Lo importante aquí es observar cómo se transmite el genérico a TextProps<C>. Similar a una llamada de función en JavaScript, pero con llaves angulares.

La varita mágica aquí es aprovechar el React.ComponentPropsWithoutReftipo como se muestra a continuación:

type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>; //  look here 

export const Text = <C extends React.ElementType>({
  as,
  children,
}: TextProps<C>) => {
  const Component = as || "span";
  return <Component>{children}</Component>;
};

Tenga en cuenta que estamos introduciendo una intersección aquí. Esencialmente, estamos diciendo que el tipo de TextPropses un tipo de objeto que contiene as, childreny algunos otros tipos representados por React.ComponentPropsWithoutRef.

Si lee el código, quizás se haga evidente lo que está pasando aquí.

Según el tipo de as, representado por el genérico C, React.componentPropsWithoutRefdevolverá accesorios de componente válidos que se correlacionan con el atributo de cadena pasado al asaccesorio.

Hay un punto más importante a tener en cuenta.

Si recién comenzó a escribir y confía en IntelliSense de su editor, se dará cuenta de que hay tres variantes del React.ComponentProps...tipo:

  1. React.ComponentProps
  2. React.ComponentPropsWithRef
  3. React.ComponentPropsWithoutRef

Si intentara usar el primero, ComponentPropsvería una nota relevante que dice:

Preferir ComponentPropsWithRef, si refse reenvía o ComponentPropsWithoutRefcuando no se admiten referencias.

Esto es precisamente lo que hemos hecho. Por ahora, ignoraremos el caso de uso para admitir un refaccesorio y nos ceñiremos a ComponentPropsWithoutRef.

¡Ahora, probemos la solución!

Si sigue adelante y usa este componente incorrectamente, por ejemplo, pasando un asaccesorio válido con otros accesorios incompatibles, obtendrá un error.

<Text as="div" href="www.google.com">Hello Text world</Text>

Un valor de dives perfectamente válido para la aspropiedad, pero a divno debería tener un hrefatributo.

Eso está mal, y TypeScript lo detectó correctamente con el error: Property 'href' does not exist on type ....

¡Esto es genial! Tenemos una solución aún mejor y más robusta.

Finalmente, asegúrese de pasar otros accesorios al elemento renderizado:

type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>; 

export const Text = <C extends React.ElementType>({
  as,
  children,
  ...restProps, //  look here
}: TextProps<C>) => {
  const Component = as || "span";

  // see restProps passed 
  return <Component {...restProps}>{children}</Component>;
};

Avancemos.

Manejo de asatributos predeterminados

Considere nuevamente nuestra solución actual:

export const Text = <C extends React.ElementType>({
  as,
  children,
  ...restProps
}: TextProps<C>) => {
  const Component = as || "span"; //  look here

  return <Component {...restProps}>{children}</Component>;
};

En particular, preste atención a dónde se proporciona un elemento predeterminado si asse omite la propiedad.

const Component = as || "span"

Esto se representa correctamente en el mundo de JavaScript mediante la implementación: si ases opcional, por defecto será un span.

La pregunta es, ¿cómo maneja TypeScript este caso cuando asno se pasa? ¿Estamos igualmente pasando un tipo predeterminado?

Bueno, la respuesta es no, pero a continuación hay un ejemplo práctico. Digamos que siguió adelante para usar el Textcomponente de la siguiente manera:

<Text>Hello Text world</Text>

Tenga en cuenta que no hemos pasado ningún asaccesorio aquí. ¿TypeScript estará al tanto de los accesorios válidos para este componente?

Sigamos adelante y agreguemos un href:

<Text href="https://www.google.com">Hello Text world</Text>

Si continúa y hace esto, no obtendrá errores. Eso es malo.

A spanno debería recibir un hrefaccesorio/atributo. Si bien tenemos un valor predeterminado spanen la implementación, TypeScript desconoce este valor predeterminado. Arreglemos esto con una asignación predeterminada simple y genérica:

type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;

/**
* See default below. TS will treat the rendered element as a 
span and provide typings accordingly
*/
export const Text = <C extends React.ElementType = "span">({
  as,
  children,
  ...restProps
}: TextProps<C>) => {
  const Component = as || "span";
  return <Component {...restProps}>{children}</Component>;
};

El bit importante se destaca a continuación:

<C extends React.ElementType = "span">

Et voila! El ejemplo anterior que teníamos ahora debería arrojar un error cuando pasa hrefal Textcomponente sin un asaccesorio.

El error debería decir: Property 'href' does not exist on type ....

Hacer que el componente sea reutilizable con sus accesorios.

Nuestra solución actual es mucho mejor que con la que comenzamos. Date una palmadita en la espalda por haber llegado tan lejos; a partir de aquí solo se vuelve más interesante.

El caso de uso para atender en esta sección es muy aplicable en el mundo real. Existe una gran posibilidad de que si está creando algún tipo de componente, ese componente también incluirá algunos accesorios específicos que son exclusivos del componente.

Nuestra solución actual tiene en cuenta el as, childreny los otros accesorios de componentes basados ​​en el asaccesorio. Sin embargo, ¿qué pasaría si quisiéramos que este componente manejara sus propios accesorios?

Hagamos esto práctico. Haremos que el Textcomponente reciba un coloraccesorio. El coloraquí será cualquiera de los colores del arco iris o black.

Continuaremos y representaremos esto de la siguiente manera:

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

A continuación, debemos definir el coloraccesorio en el TextPropsobjeto de la siguiente manera:

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black"; //  look here
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;

Antes de continuar, hagamos un poco de refactorización. Representemos los accesorios reales del Textcomponente por un Propsobjeto, y escribamos específicamente solo los accesorios específicos de nuestro componente en el TextPropsobjeto.

Esto se volverá obvio, como verás a continuación:

// new "Props" type
type Props <C extends React.ElementType> = TextProps<C>

export const Text = <C extends React.ElementType = "span">({
  as,
  children,
  ...restProps,
}: Props<C>) => {
  const Component = as || "span";
  return <Component {...restProps}>{children}</Component>;
};

Ahora vamos a limpiar TextProps:

// before 
type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black"; //  look here
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;

// after
type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black";
};

Ahora, TextPropssolo debe contener los accesorios que son específicos de nuestro Textcomponente: asy color.

Ahora debemos actualizar la definición de Propspara incluir los tipos que hemos eliminado de TextProps, es decir, childreny React.ComponentPropsWithoutRef<C>.

Para la childrenutilería, aprovecharemos la React.PropsWithChildrenutilería.

PropsWithChildrenes bastante fácil de razonar. Le pasa los accesorios de su componente y le inyectará la definición de accesorios para niños:

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>>

Tenga en cuenta cómo usamos las llaves angulares; esta es la sintaxis para pasar genéricos. Esencialmente, React.PropsWithChildrenacepta los accesorios de su componente como un genérico y lo aumenta con el childrenaccesorio. ¡Dulce!

Para React.ComponentPropsWithoutRef<C>, seguiremos adelante y aprovecharemos un tipo de intersección aquí:

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

Y aquí está la solución actual completa:

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black";
};

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

export const Text = <C extends React.ElementType = "span">({
  as,
  children,
}: Props<C>) => {
  const Component = as || "span";
  return <Component> {children} </Component>;
};

Sé que esto puede parecer mucho, pero cuando miras más de cerca, todo tendrá sentido. ¡Realmente es solo juntar todo lo que has aprendido hasta ahora!

Habiendo hecho este refactor necesario, ahora podemos continuar con nuestra solución. Lo que tenemos ahora realmente funciona. Hemos escrito explícitamente el coloraccesorio, y puede usarlo de la siguiente manera:

<Text color="violet">Hello world</Text>

Omitir estrictamente accesorios de componentes genéricos

Solo hay una cosa con la que no me siento particularmente cómodo: colorresulta ser también un atributo válido para numerosas etiquetas HTML, como era el caso antes de HTML5. Entonces, si lo eliminamos colorde nuestra definición de tipo, se aceptará como cualquier cadena válida.

Vea abajo:

type TextProps<C extends React.ElementType> = {
  as?: C;
  // remove color from the definition here
};

Ahora, si continúa usando Textcomo antes, es igualmente válido:

<Text color="violet">Hello world</Text>

La única diferencia aquí es cómo se escribe. colorahora está representado por la siguiente definición:

color?: string | undefined

Nuevamente, ¡esta NO es una definición que escribimos en nuestros tipos!

Este es un tipo de HTML predeterminado, donde colores un atributo válido para la mayoría de los elementos HTML. Consulte esta pregunta de desbordamiento de pila para obtener más contexto.

Dos posibles soluciones

Ahora, hay dos maneras de ir aquí. El primero es mantener nuestra solución inicial, donde declaramos explícitamente la colorpropiedad:

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black"; //  look here
};

Podría decirse que la segunda opción proporciona algo más de seguridad tipográfica. Para lograr esto, debes darte cuenta de dónde colorvino la definición predeterminada anterior: el React.ComponentPropsWithoutRef<C>. Esto es lo que agrega otros accesorios según el tipo as.

Entonces, con esta información, podemos eliminar explícitamente cualquier definición que exista en nuestros tipos de componentes de React.ComponentPropsWithoutRef<C>.

Esto puede ser difícil de entender antes de verlo en acción, así que vayamos paso a paso.

React.ComponentPropsWithoutRef<C>, como se indicó anteriormente, contiene todas las demás propiedades válidas basadas en el tipo de as, por ejemplo, href, color, etc., donde estos tipos tienen todas sus propias definiciones, por ejemplo, color?: string | undefined, etc.:

Es posible que algunos valores que existen en React.ComponentPropsWithoutRef<C>también existan en nuestra definición de tipo de accesorios de componente. En nuestro caso, colorexiste en ambos!

En lugar de confiar en nuestra colordefinición para anular lo que proviene de React.ComponentPropsWithoutRef<C>, eliminaremos explícitamente cualquier tipo que también exista en nuestra definición de tipos de componentes.

Por lo tanto, si existe algún tipo en nuestra definición de tipos de componentes, eliminaremos explícitamente esos tipos de React.ComponentPropsWithoutRef<C>.

Quitar tipos deReact.ComponentPropsWithoutRef<C>

Esto es lo que teníamos antes:

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

En lugar de tener un tipo de intersección donde agregamos todo lo que viene React.ComponentPropsWithoutRef<C>, seremos más selectivos. Usaremos los tipos de utilidad Omity keyofTypeScript para realizar algo de magia de TypeScript.

Echar un vistazo:

// before 
type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

// after
type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> &   
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

Esta es la parte importante:

Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

Omittoma en dos genéricos. El primero es un tipo de objeto y el segundo es una unión de tipos que le gustaría "omitir" del tipo de objeto.

Aquí está mi ejemplo favorito. Considere un Voweltipo de objeto de la siguiente manera:

type Vowels = {
  a: 'a',
  e: 'e',
  i: 'i',
  o: 'o',
  u: 'u'
}

Este es un tipo de objeto de clave y valor. Digamos que quería derivar un nuevo tipo de Vowelscalled VowelsInOhans.

Bueno, sé que el nombre Ohanscontiene dos vocales oy a. En lugar de declarar manualmente estos:

type VowelsInOhans = {
  a: 'a',
  o: 'o'
}

Puedo seguir adelante para aprovechar Omitde la siguiente manera:

type VowelsInOhans = Omit<Vowels, 'e' | 'i' | 'u'>

Omit"omitirá" las teclas e, iy udel tipo de objeto Vowels.

Por otro lado, el operador de TypeScriptkeyof funciona como te imaginas. Piense Object.keysen JavaScript: dado un objecttipo, keyofdevolverá un tipo de unión de las claves del objeto.

¡Uf! Eso es un bocado. Aquí hay un ejemplo:

type Vowels = {
  a: 'a',
  e: 'e',
  i: 'i',
  o: 'o',
  u: 'u'
}

type Vowel = keyof Vowels 

Ahora, Vowelserá un tipo de unión de las claves de Vowels, es decir:

type Vowel = 'a' | 'e' | 'i' | 'o' | 'u'

Si los junta y echa un segundo vistazo a nuestra solución, todo encajará muy bien:

Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

keyof TextProps<C>devuelve un tipo de unión de las claves de nuestro componente props. Esto a su vez se pasa a Omitpara omitirlos de React.ComponentPropsWithoutRef<C>.

¡Dulce!🕺

Para terminar, avancemos y pasemos el colorapoyo al elemento renderizado:

export const Text = <C extends React.ElementType = "span">({
  as,
  color, //  look here
  children,
  ...restProps
}: Props<C>) => {
  const Component = as || "span";

  //  compose an inline style object
  const style = color ? { style: { color } } : {};

  //  pass the inline style to the rendered element
  return (
    <Component {...restProps} {...style}>
      {children}
    </Component>
  );
};

Crear una utilidad reutilizable para tipos polimórficos

Finalmente tenemos una solución que funciona bien. Ahora, sin embargo, demos un paso más allá.

La solución que tenemos funciona muy bien para nuestro Textcomponente. Sin embargo, ¿qué sucede si prefiere tener una solución que pueda reutilizar en cualquier componente de su elección, de modo que pueda tener una solución reutilizable para cada caso de uso?

Empecemos. Primero, aquí está la solución completa actual sin anotaciones:

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black";
};

type Props<C extends React.ElementType> = React.PropsWithChildren<
  TextProps<C>
> &
  Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

export const Text = <C extends React.ElementType = "span">({
  as,
  color,
  children,
  ...restProps
}: Props<C>) => {
  const Component = as || "span";

  const style = color ? { style: { color } } : {};

  return (
    <Component {...restProps} {...style}>
      {children}
    </Component>
  );
};

Sucinto y práctico.

Si hicimos esto reutilizable, entonces tiene que funcionar para cualquier componente. Esto significa eliminar el codificado TextPropsy representarlo con un genérico, para que cualquiera pueda pasar los accesorios de componentes que necesite.

Actualmente, representamos nuestros accesorios de componentes con la definición Props<C>. Donde Crepresenta el tipo de elemento pasado para la aspropiedad.

Ahora cambiaremos eso a:

// before
Props<C>

// after 
PolymorphicProps<C, TextProps>

PolymorphicPropsrepresenta el tipo de utilidad que escribiremos en breve. Sin embargo, tenga en cuenta que esto acepta dos tipos genéricos, siendo el segundo los accesorios de componente en cuestión: TextProps.

Continúe y defina el PolymorphicPropstipo:

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = {} //  empty object for now 

La definición anterior debe ser comprensible. Crepresenta el tipo de elemento pasado en as, y Propses el componente real props, TextProps.

Primero, dividamos lo TextPropsque teníamos antes en lo siguiente:

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type TextProps = { color?: Rainbow | "black" };

Entonces, hemos separado el AsPropdel TextProps. Para ser justos, representan dos cosas diferentes. Esta es una mejor representación.

Ahora, cambiemos la PolymorphicComponentPropdefinición de la utilidad para incluir la aspropiedad, las propiedades del componente y childrenla propiedad, como lo hemos hecho en el pasado:

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>

Estoy seguro de que ahora entiendes lo que está pasando aquí: tenemos un tipo de intersección de Props(que representa los accesorios del componente) y AsProprepresenta el asaccesorio. Todos estos se pasan PropsWithChildrenpara agregar la childrendefinición de prop. ¡Excelente!

Ahora, necesitamos incluir el bit donde agregamos la React.ComponentPropsWithoutRef<C>definición. Sin embargo, debemos recordar omitir accesorios que existen en nuestra definición de componente.
Vamos a encontrar una solución robusta.

Escriba un nuevo tipo que solo comprenda los accesorios que nos gustaría omitir. Es decir, las teclas del AsPropy los accesorios del componente también.

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

¿Recuerdas el keyoftipo de utilidad?

PropsToOmitahora comprenderá un tipo de unión de los accesorios que queremos omitir, que es cada accesorio de nuestro componente representado por Py el accesorio polimórfico real as, representado por AsProps.

Ponga todo esto junto muy bien en la PolymorphicComponentPropdefinición:

type AsProp<C extends React.ElementType> = {
  as?: C;
};

// before 
type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>

// after
type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, 
   PropsToOmit<C, Props>>;

Lo importante aquí es que hemos agregado la siguiente definición:

Omit<React.ComponentPropsWithoutRef<C>, 
   PropsToOmit<C, Props>>;

Esto básicamente omite los tipos correctos de React.componentPropsWithoutRef. ¿Todavía recuerdas cómo omit funciona ?

Por simple que parezca, ¡ahora tiene una solución que puede reutilizar en múltiples componentes en diferentes proyectos!

Aquí está la implementación completa:

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

Ahora podemos continuar y usar PolymorphicComponentPropen nuestro Textcomponente de la siguiente manera:

export const Text = <C extends React.ElementType = "span">({
  as,
  color,
  children,
  // look here 
}: PolymorphicComponentProp<C, TextProps>) => {
  const Component = as || "span";
  const style = color ? { style: { color } } : {};
  return <Component {...style}>{children}</Component>;
};

¡Que agradable! Si crea otro componente, puede continuar y escribirlo así:

PolymorphicComponentProp<C, MyNewComponentProps>

¿Escuchas ese sonido? Ese es el sonido de la victoria: ¡has llegado tan lejos!

Referencias de apoyo en componentes polimórficos

¿Recuerdas todas las referencias React.ComponentPropsWithoutRefhasta ahora?😅Puntales de componentes... sin referencias. Bueno, ¡ahora es el momento de poner los árbitros!

Esta es la parte final y más compleja de nuestra solución. Necesitaré que seas paciente aquí, pero también haré todo lo posible para explicar cada paso en detalle.

Lo primero es lo primero, ¿recuerdas cómo refsfunciona React ? El concepto más importante aquí es que simplemente no pasa refcomo accesorio y espera que se transmita a su componente como cualquier otro accesorio. La forma recomendada de manejar refssus componentes funcionales es usar la forwardReffunción .

Comencemos con una nota práctica.

Si continúa y pasa un refa nuestro Textcomponente ahora, obtendrá un error que dice Property 'ref' does not exist on type ....

// Create the ref object 
const divRef = useRef<HTMLDivElement | null>(null);
... 
// Pass the ref to the rendered Text component
<Text as="div" ref={divRef}>
  Hello Text world
</Text>

Esto se espera.

Nuestra primera oportunidad de admitir referencias será usarlas forwardRefen el Textcomponente como se muestra a continuación:

// before 
export const Text = <C extends React.ElementType = "span">({
  as,
  color,
  children,
}: PolymorphicComponentProp<C, TextProps>) => {
  ...
};


// after
import React from "react";

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">({
    as,
    color,
    children,
  }: PolymorphicComponentProp<C, TextProps>) => {
    ...
  }
);

Esto es esencialmente envolver el código anterior en React.forwardRef, eso es todo.

Ahora, React.forwardReftiene la siguiente firma:

React.forwardRef((props, ref) ... )

Esencialmente, el segundo argumento recibido es el refobjeto. Sigamos adelante y manejemos eso:

type PolymorphicRef<C extends React.ElementType> = unknown;

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: PolymorphicComponentProp<C, TextProps>,
    //  look here
    ref?: PolymorphicRef<C>
  ) => {
    ...
  }
);

Lo que hemos hecho aquí es agregar el segundo argumento, refy declarar su tipo como PolymorphicRef, que solo apunta a unknownpor ahora.

Tenga en cuenta que PolymorphicReftoma en el genérico C. Esto es similar a las soluciones anteriores: el refobjeto de a divdifiere del de a span, por lo que debemos tener en cuenta el tipo de elemento que se pasa a la aspropiedad.

Apunte su atención al PolymorphicReftipo. ¿Cómo podemos obtener el reftipo de objeto en función de la aspropiedad?

Déjame darte una pista: React.ComponentPropsWithRef!

Tenga en cuenta que esto dice con ref. No sin ref.

Esencialmente, si se tratara de un paquete de claves (que, de hecho, lo es), incluirá todos los accesorios de componentes relevantes según el tipo de elemento, además del objeto ref.

Entonces, si sabemos que este tipo de objeto contiene la refclave, también podemos obtener ese tipo de referencia haciendo lo siguiente:

// before 
type PolymorphicRef<C extends React.ElementType> = unknown;

// after 
type PolymorphicRef<C extends React.ElementType> =
  React.ComponentPropsWithRef<C>["ref"];

Esencialmente, React.ComponentPropsWithRef<C>devuelve un tipo de objeto, por ejemplo,

{
  ref: SomeRefDefinition, 
  // ... other keys, 
  color: string 
  href: string 
  // ... etc
}

Para seleccionar solo el reftipo, podemos hacer esto:

React.ComponentPropsWithRef<C>["ref"];

Tenga en cuenta que la sintaxis es similar a la sintaxis del acceso a la propiedad en JavaScript, es decir, ["ref"]. Ahora que tenemos el refaccesorio escrito, podemos continuar y pasarlo al elemento renderizado:

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: PolymorphicComponentProp<C, TextProps>,
    ref?: PolymorphicRef<C>
  ) => {
    //...

    return (
      <Component {...style} ref={ref}> //  look here
        {children}
      </Component>
    );
  }
);

¡Hemos hecho un progreso decente! De hecho, si continúa y verifica el uso de Textcomo lo hicimos antes, no habrá más errores:

// create the ref object 
const divRef = useRef<HTMLDivElement | null>(null);
... 
// pass ref to the rendered Text component
<Text as="div" ref={divRef}>
  Hello Text world
</Text>

Sin embargo, nuestra solución todavía no está tan fuertemente tipada como me gustaría. Avancemos y cambiemos la referencia pasada a Textcomo se muestra a continuación:

// create a "button" ref object 
const buttonRef = useRef<HTMLButtonElement | null>(null);
... 
// pass a button ref to a "div". NB: as = "div"
<Text as="div" ref={buttonRef}>
  Hello Text world
</Text>

TypeScript debería arrojar un error aquí, pero no lo hace. Estamos creando una buttonreferencia, pero pasándola a un divelemento. Eso no está bien.

Si observa el tipo exacto de ref, se ve así:

React.RefAttributes<unknown>.ref?: React.Ref<unknown>

¿Ves el unknownde ahí? Esa es una señal de tipeo débil. Idealmente, deberíamos tener HTMLDivElementallí para definir explícitamente el objeto ref como un divelemento ref.

Tenemos trabajo que hacer. Primero veamos los tipos de los otros accesorios del Textcomponente, que aún hacen referencia al PolymorphicComponentProptipo. Cambie esto a un nuevo tipo llamado PolymorphicComponentPropWithRef. Esto será solo una unión de PolymorphicComponentPropy el ref prop. (Lo adivinaste.)

Aquí está:

type PolymorphicComponentPropWithRef<
  C extends React.ElementType,
  Props = {}
> = PolymorphicComponentProp<C, Props> & 
{ ref?: PolymorphicRef<C> };

Esta es solo una unión de la anterior PolymorphicComponentPropy { ref?: PolymorphicRef<C> }.

Ahora necesitamos cambiar los accesorios del componente para hacer referencia al nuevo PolymorphicComponentPropWithReftipo:

// before
type TextProps = { color?: Rainbow | "black" };

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: PolymorphicComponentProp<C, TextProps>,
    ref?: PolymorphicRef<C>
  ) => {
    ...
  }
);


// now 
type TextProps<C extends React.ElementType> = 
PolymorphicComponentPropWithRef<
  C,
  { color?: Rainbow | "black" }
>;

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: TextProps<C>, //  look here
    ref?: PolymorphicRef<C>
  ) => {
    ...
  }
);

Hemos actualizado TextPropsa la referencia PolymorphicComponentPropWithRefy eso ahora se pasa como accesorios para el Textcomponente. ¡Hermoso!

Queda una última cosa por hacer: proporcione una anotación de tipo para el Textcomponente. Se parece a:

export const Text : TextComponent = ...

TextComponentes el tipo de anotación que escribiremos. Aquí está completamente escrito:

type TextComponent = <C extends React.ElementType = "span">(
  props: TextProps<C>
) => React.ReactElement | null;

Este es esencialmente un componente funcional que toma TextPropsy devuelve React.ReactElement | null, donde TextPropses como se definió anteriormente:

type TextProps<C extends React.ElementType> = 
PolymorphicComponentPropWithRef<
  C,
  { color?: Rainbow | "black" }
>;

¡Con esto, ahora tenemos una solución completa!

Voy a compartir la solución completa ahora. Puede parecer abrumador al principio, pero recuerde que hemos trabajado línea por línea en todo lo que ve aquí. Léalo con esa confianza.

import React from "react";

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

// This is the first reusable type utility we built
type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

// This is a new type utitlity with ref!
type PolymorphicComponentPropWithRef<
  C extends React.ElementType,
  Props = {}
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };

// This is the type for the "ref" only
type PolymorphicRef<C extends React.ElementType> =
  React.ComponentPropsWithRef<C>["ref"];

/**
* This is the updated component props using PolymorphicComponentPropWithRef
*/
type TextProps<C extends React.ElementType> = 
PolymorphicComponentPropWithRef<
  C,
  { color?: Rainbow | "black" }
>;

/**
* This is the type used in the type annotation for the component
*/
type TextComponent = <C extends React.ElementType = "span">(
  props: TextProps<C>
) => React.ReactElement | null;

export const Text: TextComponent = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: TextProps<C>,
    ref?: PolymorphicRef<C>
  ) => {
    const Component = as || "span";

    const style = color ? { style: { color } } : {};

    return (
      <Component {...style} ref={ref}>
        {children}
      </Component>
    );
  }
);

¡Y ahí tienes!

Conclusión e ideas para los próximos pasos.

Ha creado con éxito una solución robusta para manejar componentes polimórficos en React con TypeScript. Sé que no fue un viaje fácil, pero lo lograste.

Gracias por seguirnos.

Esta historia se publicó originalmente en https://blog.logrocket.com/build-strongly-typed-polymorphic-components-react-typescript/

 #typescript #react-native 

What is GEEK

Buddha Community

Componentes Polimórficos Fuertemente Tipados Con React Y TypeScript
Autumn  Blick

Autumn Blick

1598839687

How native is React Native? | React Native vs Native App Development

If you are undertaking a mobile app development for your start-up or enterprise, you are likely wondering whether to use React Native. As a popular development framework, React Native helps you to develop near-native mobile apps. However, you are probably also wondering how close you can get to a native app by using React Native. How native is React Native?

In the article, we discuss the similarities between native mobile development and development using React Native. We also touch upon where they differ and how to bridge the gaps. Read on.

A brief introduction to React Native

Let’s briefly set the context first. We will briefly touch upon what React Native is and how it differs from earlier hybrid frameworks.

React Native is a popular JavaScript framework that Facebook has created. You can use this open-source framework to code natively rendering Android and iOS mobile apps. You can use it to develop web apps too.

Facebook has developed React Native based on React, its JavaScript library. The first release of React Native came in March 2015. At the time of writing this article, the latest stable release of React Native is 0.62.0, and it was released in March 2020.

Although relatively new, React Native has acquired a high degree of popularity. The “Stack Overflow Developer Survey 2019” report identifies it as the 8th most loved framework. Facebook, Walmart, and Bloomberg are some of the top companies that use React Native.

The popularity of React Native comes from its advantages. Some of its advantages are as follows:

  • Performance: It delivers optimal performance.
  • Cross-platform development: You can develop both Android and iOS apps with it. The reuse of code expedites development and reduces costs.
  • UI design: React Native enables you to design simple and responsive UI for your mobile app.
  • 3rd party plugins: This framework supports 3rd party plugins.
  • Developer community: A vibrant community of developers support React Native.

Why React Native is fundamentally different from earlier hybrid frameworks

Are you wondering whether React Native is just another of those hybrid frameworks like Ionic or Cordova? It’s not! React Native is fundamentally different from these earlier hybrid frameworks.

React Native is very close to native. Consider the following aspects as described on the React Native website:

  • Access to many native platforms features: The primitives of React Native render to native platform UI. This means that your React Native app will use many native platform APIs as native apps would do.
  • Near-native user experience: React Native provides several native components, and these are platform agnostic.
  • The ease of accessing native APIs: React Native uses a declarative UI paradigm. This enables React Native to interact easily with native platform APIs since React Native wraps existing native code.

Due to these factors, React Native offers many more advantages compared to those earlier hybrid frameworks. We now review them.

#android app #frontend #ios app #mobile app development #benefits of react native #is react native good for mobile app development #native vs #pros and cons of react native #react mobile development #react native development #react native experience #react native framework #react native ios vs android #react native pros and cons #react native vs android #react native vs native #react native vs native performance #react vs native #why react native #why use react native

Diego  Elizondo

Diego Elizondo

1654632000

Componentes Polimórficos Fuertemente Tipados Con React Y TypeScript

En esta guía detallada (y explicativa), discutiré cómo construir componentes React polimórficos fuertemente tipados con TypeScript.

Como puede ver, esto es bastante largo, así que siéntase libre de saltearlo. Si desea seguir, marque el repositorio de código oficial en mi GitHub como referencia.

Ejemplos del mundo real de componentes polimórficos

Existe una probabilidad distinta de cero de que ya haya usado un componente polimórfico. Las bibliotecas de componentes de código abierto normalmente implementan algún tipo de componente polimórfico.

Consideremos algunos con los que puede estar familiarizado: el asaccesorio Chakra UI y MUI component.

Accesorio de Chakra asUI

¿Cómo implementa Chakra UI accesorios polimórficos? La respuesta es exponer un asaccesorio. El asaccesorio se pasa a un componente para determinar qué elemento contenedor debería representar eventualmente.

Todo lo que necesita hacer para usar el asaccesorio es pasarlo al componente, que en este caso es Box:

<Box as='button' borderRadius='md' bg='tomato' color='white' px={4} h={8}>
  Button
</Box>

Ahora, el componente representará un buttonelemento.

Si cambiaste el asaccesorio a un h1:

<Box as="h1"> Hello </Box>

Ahora, el Boxcomponente representa un h1:

¡Eso es un componente polimórfico en el trabajo! Se puede representar en elementos completamente únicos, todo al pasar un solo accesorio.

apoyo de MUIcomponent

Similar a Chakra UI, MUI permite un accesorio polimórfico llamado component, que se implementa de manera similar: lo pasa a un componente y establece el elemento o componente personalizado que le gustaría representar.

Aquí hay un ejemplo de los documentos oficiales :

<List component="nav">
  <ListItem button>
    <ListItemText primary="Trash" />
  </ListItem>
</List>

Listse pasa un componente prop de nav; cuando esto se represente, generará un navelemento contenedor.

Otro usuario puede usar el mismo componente, pero no para la navegación; en su lugar, es posible que deseen generar una lista de tareas pendientes:

<List component="ol">
  ...
</List>

Y en este caso, Listrepresentará un elemento de lista ordenada ol.

¡Hablando de flexibilidad! Consulte un resumen de los casos de uso de los componentes polimórficos.

Como verá en las siguientes secciones de este artículo, los componentes polimórficos son poderosos. Además de aceptar un accesorio de un tipo de elemento, también pueden aceptar componentes personalizados como accesorios.

Esto se discutirá en una próxima sección de este artículo. Por ahora, ¡construyamos nuestro primer componente polimórfico!

Construcción de un componente polimórfico simple

Al contrario de lo que pueda pensar, construir su primer componente polimórfico es bastante sencillo. Aquí hay una implementación básica:

const MyComponent = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Tenga en cuenta aquí que el accesorio polimórfico ases similar a la interfaz de usuario de Chakra. Este es el accesorio que exponemos para controlar el elemento de representación del componente polimórfico.

En segundo lugar, tenga en cuenta que el asaccesorio no se representa directamente. Lo siguiente estaría mal:

const MyComponent = ({ as, children }) => {
  // wrong render below  
  return <as>{children}</as>;
};

Al renderizar un tipo de elemento en tiempo de ejecución , primero debe asignarlo a una variable en mayúsculas y luego renderizar la variable en mayúsculas.

Ahora puede continuar y usar este componente de la siguiente manera:

<MyComponent as="button">Hello Polymorphic!<MyComponent>
<MyComponent as="div">Hello Polymorphic!</MyComponent>
<MyComponent as="span">Hello Polymorphic!</MyComponent>
<MyComponent as="em">Hello Polymorphic!</MyComponent>

Tenga en cuenta que el asaccesorio diferente se pasa a los componentes representados arriba.

Problemas con esta implementación simple

La implementación de la sección anterior, aunque bastante estándar, tiene muchos inconvenientes. Exploremos algunos de estos.

1. El asaccesorio puede recibir elementos HTML no válidos

Actualmente, es posible que un usuario escriba lo siguiente:

<MyComponent as="emmanuel">Hello Wrong Element</MyComponent>

El asprop pasado aquí es emmanuel. Emmanuel es obviamente un elemento HTML incorrecto, pero el navegador también intenta representar este elemento.

Una experiencia de desarrollo ideal es mostrar algún tipo de error durante el desarrollo. Por ejemplo, un usuario puede cometer un error tipográfico simple, divven lugar de div, y no obtendría ninguna indicación de lo que está mal.

2. Se pueden pasar atributos incorrectos para elementos válidos

Considere el siguiente uso de componentes:

<MyComponent as="span" href="https://www.google.com">
   Hello Wrong Attribute
</MyComponent>

Un consumidor puede pasar un spanelemento a la aspropiedad y hreftambién una propiedad.

Esto es técnicamente inválido. Un spanelemento no toma (y no debería tomar) un hrefatributo. Esa es una sintaxis HTML no válida. Sin embargo, un consumidor del componente que hemos creado aún podría escribir esto y no recibir errores durante el desarrollo.

3. ¡Sin soporte de atributos!

Considere la implementación simple nuevamente:

const MyComponent = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Los únicos accesorios que acepta este componente son asy children, nada más. No hay compatibilidad con atributos ni siquiera aspara accesorios de elementos válidos, es decir, si asfuera un elemento de anclaje a, también deberíamos admitir pasar an hrefal componente.

<MyComponent as="a" href="...">A link </MyComponent>

Aunque hrefse pasa en el ejemplo anterior, la implementación del componente no recibe otros apoyos. Sólo asy childrense deconstruyen.

Sus pensamientos iniciales pueden ser seguir adelante y distribuir todos los demás apoyos pasados ​​al componente de la siguiente manera:

const MyComponent = ({ as, children, ...rest }) => {
  const Component = as || "span";

  return <Component {...rest}>{children}</Component>;
};

Esto parece una solución decente, pero ahora resalta el segundo problema mencionado anteriormente. Los atributos incorrectos ahora también se transmitirán al componente.

Considera lo siguiente:

<MyComponent as="span" href="https://www.google.com">
   Hello Wrong Attribute
</MyComponent>

Y tenga en cuenta el marcado renderizado eventual:

A spancon un hrefes HTML no válido.

¿Por qué es esto malo?

En resumen, los problemas actuales con nuestra implementación simple son insatisfactorios porque:

  • Proporciona una experiencia de desarrollador terrible
  • No es de tipo seguro. Los errores pueden (y lo harán) colarse

¿Cómo resolvemos estas preocupaciones? Para ser claros, no hay una varita mágica para agitar aquí. Sin embargo, aprovecharemos TypeScript para garantizar que construya componentes polimórficos fuertemente tipados.

Al finalizar, los desarrolladores que usen sus componentes evitarán los errores de tiempo de ejecución anteriores y, en su lugar, los detectarán durante el desarrollo o el tiempo de compilación, todo gracias a TypeScript.

Cómo usar TypeScript para construir componentes polimórficos fuertemente tipados en React

Si está leyendo esto, un requisito previo es que ya sepa algo de TypeScript, al menos los conceptos básicos. Si no tiene idea de qué es TypeScript, le recomiendo que lea este documento primero.

En esta sección, utilizaremos TypeScript para resolver los problemas antes mencionados y construir componentes polimórficos fuertemente tipados.

Los primeros dos requisitos con los que comenzaremos incluyen:

  • La aspropiedad no debe recibir cadenas de elementos HTML no válidas.
  • No se deben pasar atributos incorrectos para elementos válidos

En la siguiente sección, presentaremos los genéricos de TypeScript para hacer que nuestra solución sea más robusta, fácil de usar para desarrolladores y digna de producción.

Asegurarse de que la aspropiedad solo reciba cadenas de elementos HTML válidas

Esta es nuestra solución actual:

const MyComponent = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Para que las próximas secciones de esta guía sean prácticas, cambiaremos el nombre del componente de MyComponenta Texty supondremos que estamos construyendo un Textcomponente polimórfico.

const Text = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Ahora, con su conocimiento de los genéricos, se vuelve obvio que es mejor representar ascon un tipo genérico, es decir, un tipo de variable basado en lo que el usuario pase.

Sigamos adelante y demos el primer paso de la siguiente manera:

export const Text = <C>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Tenga en cuenta cómo se define el genérico Cy luego se transmite en la definición de tipo para el accesorio as.

Sin embargo, si escribió este código aparentemente perfecto, TypeScript gritará numerosos errores con más líneas rojas onduladas de lo que le gustaría.🤷‍♀️

Lo que está pasando aquí es una falla en la sintaxis de los genéricos en los .tsxarchivos. Hay dos formas de resolver esto.

1. Agregue una coma después de la declaración genérica

Esta es la sintaxis para declarar varios genéricos. Una vez que haga esto, el compilador de TypeScript entiende claramente su intención y los errores desaparecen.

// note the comma after "C" below 
export const Text = <C,>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

2. Restringir lo genérico

La segunda opción es restringir el genérico como mejor le parezca. Para empezar, puede usar el unknowntipo de la siguiente manera:

// note the extends keyword below 
export const Text = <C extends unknown>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Por ahora, me ceñiré a la segunda solución porque está más cerca de nuestra solución final. Sin embargo, en la mayoría de los casos, uso la sintaxis genérica múltiple y solo agrego una coma.

Sin embargo, con nuestra solución actual, obtenemos otro error de TypeScript:

El tipo de elemento JSX 'Componente' no tiene ninguna construcción ni firma de llamada. ts(2604)

Esto es similar al error que tuvimos cuando trabajamos con la echoLengthfunción. Al igual que acceder a la lengthpropiedad de un tipo de variable desconocido, se puede decir lo mismo aquí: tratar de representar cualquier tipo genérico como un componente React válido no tiene sentido.

Necesitamos restringir el genérico solo para que se ajuste al molde de un tipo de elemento React válido.

Para lograr esto, aprovecharemos el tipo React interno: React.ElementTypey nos aseguraremos de que el genérico esté restringido para adaptarse a ese tipo:

// look just after the extends keyword 
export const Text = <C extends React.ElementType>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

Tenga en cuenta que si está utilizando una versión anterior de React, es posible que deba importar una versión más nueva de React de la siguiente manera:

import React from 'react'

¡Con esto, no tenemos más errores!

Ahora, si continúa y usa este componente de la siguiente manera, funcionará bien:

<Text as="div">Hello Text world</Text>

Sin embargo, si pasa una propiedad no válida as, ahora obtendrá un error de TypeScript apropiado. Considere el siguiente ejemplo:

<Text as="emmanuel">Hello Text world</Text>

Y el error arrojado:

El tipo '”emmanuel”' no se puede asignar al tipo 'ElementType | indefinido'.


¡Esto es excelente! Ahora tenemos una solución que no acepta galimatías para el asaccesorio y también evitará errores tipográficos desagradables, por ejemplo, divven lugar de div.

¡Esta es una experiencia de desarrollador mucho mejor!

Manejo de atributos de componentes válidos con genéricos de TypeScript

Al resolver este segundo caso de uso, llegará a apreciar cuán poderosos son realmente los genéricos. Primero, entendamos lo que estamos tratando de lograr aquí.

Una vez que recibimos un astipo genérico, queremos asegurarnos de que los accesorios restantes pasados ​​a nuestro componente sean relevantes, según el asaccesorio.

Entonces, por ejemplo, si un usuario pasó un asaccesorio de img, ¡ hrefquerríamos ser igualmente un accesorio válido!

Para darle una idea de cómo lograríamos esto, eche un vistazo al estado actual de nuestra solución:

export const Text = <C extends React.ElementType>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

La propiedad de este componente ahora está representada por el tipo de objeto:

{
  as?: C;
  children: React.ReactNode;
}

En pseudocódigo, lo que nos gustaría sería lo siguiente:

{
  as?: C;
  children: React.ReactNode;
} & {
  ...otherValidPropsBasedOnTheValueOfAs
}

Este requisito es suficiente para dejar a uno aferrado a un clavo ardiendo. No podemos escribir una función que determine los tipos apropiados en función del valor de as, y no es inteligente enumerar un tipo de unión manualmente.

Bueno, ¿qué pasaría si hubiera un tipo proporcionado Reactque actuara como una "función" que devolverá tipos de elementos válidos en función de lo que le pases?

Antes de presentar la solución, refactoricemos un poco. Saquemos los accesorios del componente en un tipo separado:

//  See TextProps pulled out below 
type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} 

export const Text = <C extends React.ElementType>({
  as,
  children,
}: TextProps<C>) => { //  see TextProps used 
  const Component = as || "span";
  return <Component>{children}</Component>;
};

Lo importante aquí es observar cómo se transmite el genérico a TextProps<C>. Similar a una llamada de función en JavaScript, pero con llaves angulares.

La varita mágica aquí es aprovechar el React.ComponentPropsWithoutReftipo como se muestra a continuación:

type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>; //  look here 

export const Text = <C extends React.ElementType>({
  as,
  children,
}: TextProps<C>) => {
  const Component = as || "span";
  return <Component>{children}</Component>;
};

Tenga en cuenta que estamos introduciendo una intersección aquí. Esencialmente, estamos diciendo que el tipo de TextPropses un tipo de objeto que contiene as, childreny algunos otros tipos representados por React.ComponentPropsWithoutRef.

Si lee el código, quizás se haga evidente lo que está pasando aquí.

Según el tipo de as, representado por el genérico C, React.componentPropsWithoutRefdevolverá accesorios de componente válidos que se correlacionan con el atributo de cadena pasado al asaccesorio.

Hay un punto más importante a tener en cuenta.

Si recién comenzó a escribir y confía en IntelliSense de su editor, se dará cuenta de que hay tres variantes del React.ComponentProps...tipo:

  1. React.ComponentProps
  2. React.ComponentPropsWithRef
  3. React.ComponentPropsWithoutRef

Si intentara usar el primero, ComponentPropsvería una nota relevante que dice:

Preferir ComponentPropsWithRef, si refse reenvía o ComponentPropsWithoutRefcuando no se admiten referencias.

Esto es precisamente lo que hemos hecho. Por ahora, ignoraremos el caso de uso para admitir un refaccesorio y nos ceñiremos a ComponentPropsWithoutRef.

¡Ahora, probemos la solución!

Si sigue adelante y usa este componente incorrectamente, por ejemplo, pasando un asaccesorio válido con otros accesorios incompatibles, obtendrá un error.

<Text as="div" href="www.google.com">Hello Text world</Text>

Un valor de dives perfectamente válido para la aspropiedad, pero a divno debería tener un hrefatributo.

Eso está mal, y TypeScript lo detectó correctamente con el error: Property 'href' does not exist on type ....

¡Esto es genial! Tenemos una solución aún mejor y más robusta.

Finalmente, asegúrese de pasar otros accesorios al elemento renderizado:

type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>; 

export const Text = <C extends React.ElementType>({
  as,
  children,
  ...restProps, //  look here
}: TextProps<C>) => {
  const Component = as || "span";

  // see restProps passed 
  return <Component {...restProps}>{children}</Component>;
};

Avancemos.

Manejo de asatributos predeterminados

Considere nuevamente nuestra solución actual:

export const Text = <C extends React.ElementType>({
  as,
  children,
  ...restProps
}: TextProps<C>) => {
  const Component = as || "span"; //  look here

  return <Component {...restProps}>{children}</Component>;
};

En particular, preste atención a dónde se proporciona un elemento predeterminado si asse omite la propiedad.

const Component = as || "span"

Esto se representa correctamente en el mundo de JavaScript mediante la implementación: si ases opcional, por defecto será un span.

La pregunta es, ¿cómo maneja TypeScript este caso cuando asno se pasa? ¿Estamos igualmente pasando un tipo predeterminado?

Bueno, la respuesta es no, pero a continuación hay un ejemplo práctico. Digamos que siguió adelante para usar el Textcomponente de la siguiente manera:

<Text>Hello Text world</Text>

Tenga en cuenta que no hemos pasado ningún asaccesorio aquí. ¿TypeScript estará al tanto de los accesorios válidos para este componente?

Sigamos adelante y agreguemos un href:

<Text href="https://www.google.com">Hello Text world</Text>

Si continúa y hace esto, no obtendrá errores. Eso es malo.

A spanno debería recibir un hrefaccesorio/atributo. Si bien tenemos un valor predeterminado spanen la implementación, TypeScript desconoce este valor predeterminado. Arreglemos esto con una asignación predeterminada simple y genérica:

type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;

/**
* See default below. TS will treat the rendered element as a 
span and provide typings accordingly
*/
export const Text = <C extends React.ElementType = "span">({
  as,
  children,
  ...restProps
}: TextProps<C>) => {
  const Component = as || "span";
  return <Component {...restProps}>{children}</Component>;
};

El bit importante se destaca a continuación:

<C extends React.ElementType = "span">

Et voila! El ejemplo anterior que teníamos ahora debería arrojar un error cuando pasa hrefal Textcomponente sin un asaccesorio.

El error debería decir: Property 'href' does not exist on type ....

Hacer que el componente sea reutilizable con sus accesorios.

Nuestra solución actual es mucho mejor que con la que comenzamos. Date una palmadita en la espalda por haber llegado tan lejos; a partir de aquí solo se vuelve más interesante.

El caso de uso para atender en esta sección es muy aplicable en el mundo real. Existe una gran posibilidad de que si está creando algún tipo de componente, ese componente también incluirá algunos accesorios específicos que son exclusivos del componente.

Nuestra solución actual tiene en cuenta el as, childreny los otros accesorios de componentes basados ​​en el asaccesorio. Sin embargo, ¿qué pasaría si quisiéramos que este componente manejara sus propios accesorios?

Hagamos esto práctico. Haremos que el Textcomponente reciba un coloraccesorio. El coloraquí será cualquiera de los colores del arco iris o black.

Continuaremos y representaremos esto de la siguiente manera:

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

A continuación, debemos definir el coloraccesorio en el TextPropsobjeto de la siguiente manera:

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black"; //  look here
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;

Antes de continuar, hagamos un poco de refactorización. Representemos los accesorios reales del Textcomponente por un Propsobjeto, y escribamos específicamente solo los accesorios específicos de nuestro componente en el TextPropsobjeto.

Esto se volverá obvio, como verás a continuación:

// new "Props" type
type Props <C extends React.ElementType> = TextProps<C>

export const Text = <C extends React.ElementType = "span">({
  as,
  children,
  ...restProps,
}: Props<C>) => {
  const Component = as || "span";
  return <Component {...restProps}>{children}</Component>;
};

Ahora vamos a limpiar TextProps:

// before 
type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black"; //  look here
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;

// after
type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black";
};

Ahora, TextPropssolo debe contener los accesorios que son específicos de nuestro Textcomponente: asy color.

Ahora debemos actualizar la definición de Propspara incluir los tipos que hemos eliminado de TextProps, es decir, childreny React.ComponentPropsWithoutRef<C>.

Para la childrenutilería, aprovecharemos la React.PropsWithChildrenutilería.

PropsWithChildrenes bastante fácil de razonar. Le pasa los accesorios de su componente y le inyectará la definición de accesorios para niños:

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>>

Tenga en cuenta cómo usamos las llaves angulares; esta es la sintaxis para pasar genéricos. Esencialmente, React.PropsWithChildrenacepta los accesorios de su componente como un genérico y lo aumenta con el childrenaccesorio. ¡Dulce!

Para React.ComponentPropsWithoutRef<C>, seguiremos adelante y aprovecharemos un tipo de intersección aquí:

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

Y aquí está la solución actual completa:

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black";
};

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

export const Text = <C extends React.ElementType = "span">({
  as,
  children,
}: Props<C>) => {
  const Component = as || "span";
  return <Component> {children} </Component>;
};

Sé que esto puede parecer mucho, pero cuando miras más de cerca, todo tendrá sentido. ¡Realmente es solo juntar todo lo que has aprendido hasta ahora!

Habiendo hecho este refactor necesario, ahora podemos continuar con nuestra solución. Lo que tenemos ahora realmente funciona. Hemos escrito explícitamente el coloraccesorio, y puede usarlo de la siguiente manera:

<Text color="violet">Hello world</Text>

Omitir estrictamente accesorios de componentes genéricos

Solo hay una cosa con la que no me siento particularmente cómodo: colorresulta ser también un atributo válido para numerosas etiquetas HTML, como era el caso antes de HTML5. Entonces, si lo eliminamos colorde nuestra definición de tipo, se aceptará como cualquier cadena válida.

Vea abajo:

type TextProps<C extends React.ElementType> = {
  as?: C;
  // remove color from the definition here
};

Ahora, si continúa usando Textcomo antes, es igualmente válido:

<Text color="violet">Hello world</Text>

La única diferencia aquí es cómo se escribe. colorahora está representado por la siguiente definición:

color?: string | undefined

Nuevamente, ¡esta NO es una definición que escribimos en nuestros tipos!

Este es un tipo de HTML predeterminado, donde colores un atributo válido para la mayoría de los elementos HTML. Consulte esta pregunta de desbordamiento de pila para obtener más contexto.

Dos posibles soluciones

Ahora, hay dos maneras de ir aquí. El primero es mantener nuestra solución inicial, donde declaramos explícitamente la colorpropiedad:

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black"; //  look here
};

Podría decirse que la segunda opción proporciona algo más de seguridad tipográfica. Para lograr esto, debes darte cuenta de dónde colorvino la definición predeterminada anterior: el React.ComponentPropsWithoutRef<C>. Esto es lo que agrega otros accesorios según el tipo as.

Entonces, con esta información, podemos eliminar explícitamente cualquier definición que exista en nuestros tipos de componentes de React.ComponentPropsWithoutRef<C>.

Esto puede ser difícil de entender antes de verlo en acción, así que vayamos paso a paso.

React.ComponentPropsWithoutRef<C>, como se indicó anteriormente, contiene todas las demás propiedades válidas basadas en el tipo de as, por ejemplo, href, color, etc., donde estos tipos tienen todas sus propias definiciones, por ejemplo, color?: string | undefined, etc.:

Es posible que algunos valores que existen en React.ComponentPropsWithoutRef<C>también existan en nuestra definición de tipo de accesorios de componente. En nuestro caso, colorexiste en ambos!

En lugar de confiar en nuestra colordefinición para anular lo que proviene de React.ComponentPropsWithoutRef<C>, eliminaremos explícitamente cualquier tipo que también exista en nuestra definición de tipos de componentes.

Por lo tanto, si existe algún tipo en nuestra definición de tipos de componentes, eliminaremos explícitamente esos tipos de React.ComponentPropsWithoutRef<C>.

Quitar tipos deReact.ComponentPropsWithoutRef<C>

Esto es lo que teníamos antes:

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

En lugar de tener un tipo de intersección donde agregamos todo lo que viene React.ComponentPropsWithoutRef<C>, seremos más selectivos. Usaremos los tipos de utilidad Omity keyofTypeScript para realizar algo de magia de TypeScript.

Echar un vistazo:

// before 
type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

// after
type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> &   
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

Esta es la parte importante:

Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

Omittoma en dos genéricos. El primero es un tipo de objeto y el segundo es una unión de tipos que le gustaría "omitir" del tipo de objeto.

Aquí está mi ejemplo favorito. Considere un Voweltipo de objeto de la siguiente manera:

type Vowels = {
  a: 'a',
  e: 'e',
  i: 'i',
  o: 'o',
  u: 'u'
}

Este es un tipo de objeto de clave y valor. Digamos que quería derivar un nuevo tipo de Vowelscalled VowelsInOhans.

Bueno, sé que el nombre Ohanscontiene dos vocales oy a. En lugar de declarar manualmente estos:

type VowelsInOhans = {
  a: 'a',
  o: 'o'
}

Puedo seguir adelante para aprovechar Omitde la siguiente manera:

type VowelsInOhans = Omit<Vowels, 'e' | 'i' | 'u'>

Omit"omitirá" las teclas e, iy udel tipo de objeto Vowels.

Por otro lado, el operador de TypeScriptkeyof funciona como te imaginas. Piense Object.keysen JavaScript: dado un objecttipo, keyofdevolverá un tipo de unión de las claves del objeto.

¡Uf! Eso es un bocado. Aquí hay un ejemplo:

type Vowels = {
  a: 'a',
  e: 'e',
  i: 'i',
  o: 'o',
  u: 'u'
}

type Vowel = keyof Vowels 

Ahora, Vowelserá un tipo de unión de las claves de Vowels, es decir:

type Vowel = 'a' | 'e' | 'i' | 'o' | 'u'

Si los junta y echa un segundo vistazo a nuestra solución, todo encajará muy bien:

Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

keyof TextProps<C>devuelve un tipo de unión de las claves de nuestro componente props. Esto a su vez se pasa a Omitpara omitirlos de React.ComponentPropsWithoutRef<C>.

¡Dulce!🕺

Para terminar, avancemos y pasemos el colorapoyo al elemento renderizado:

export const Text = <C extends React.ElementType = "span">({
  as,
  color, //  look here
  children,
  ...restProps
}: Props<C>) => {
  const Component = as || "span";

  //  compose an inline style object
  const style = color ? { style: { color } } : {};

  //  pass the inline style to the rendered element
  return (
    <Component {...restProps} {...style}>
      {children}
    </Component>
  );
};

Crear una utilidad reutilizable para tipos polimórficos

Finalmente tenemos una solución que funciona bien. Ahora, sin embargo, demos un paso más allá.

La solución que tenemos funciona muy bien para nuestro Textcomponente. Sin embargo, ¿qué sucede si prefiere tener una solución que pueda reutilizar en cualquier componente de su elección, de modo que pueda tener una solución reutilizable para cada caso de uso?

Empecemos. Primero, aquí está la solución completa actual sin anotaciones:

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black";
};

type Props<C extends React.ElementType> = React.PropsWithChildren<
  TextProps<C>
> &
  Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

export const Text = <C extends React.ElementType = "span">({
  as,
  color,
  children,
  ...restProps
}: Props<C>) => {
  const Component = as || "span";

  const style = color ? { style: { color } } : {};

  return (
    <Component {...restProps} {...style}>
      {children}
    </Component>
  );
};

Sucinto y práctico.

Si hicimos esto reutilizable, entonces tiene que funcionar para cualquier componente. Esto significa eliminar el codificado TextPropsy representarlo con un genérico, para que cualquiera pueda pasar los accesorios de componentes que necesite.

Actualmente, representamos nuestros accesorios de componentes con la definición Props<C>. Donde Crepresenta el tipo de elemento pasado para la aspropiedad.

Ahora cambiaremos eso a:

// before
Props<C>

// after 
PolymorphicProps<C, TextProps>

PolymorphicPropsrepresenta el tipo de utilidad que escribiremos en breve. Sin embargo, tenga en cuenta que esto acepta dos tipos genéricos, siendo el segundo los accesorios de componente en cuestión: TextProps.

Continúe y defina el PolymorphicPropstipo:

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = {} //  empty object for now 

La definición anterior debe ser comprensible. Crepresenta el tipo de elemento pasado en as, y Propses el componente real props, TextProps.

Primero, dividamos lo TextPropsque teníamos antes en lo siguiente:

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type TextProps = { color?: Rainbow | "black" };

Entonces, hemos separado el AsPropdel TextProps. Para ser justos, representan dos cosas diferentes. Esta es una mejor representación.

Ahora, cambiemos la PolymorphicComponentPropdefinición de la utilidad para incluir la aspropiedad, las propiedades del componente y childrenla propiedad, como lo hemos hecho en el pasado:

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>

Estoy seguro de que ahora entiendes lo que está pasando aquí: tenemos un tipo de intersección de Props(que representa los accesorios del componente) y AsProprepresenta el asaccesorio. Todos estos se pasan PropsWithChildrenpara agregar la childrendefinición de prop. ¡Excelente!

Ahora, necesitamos incluir el bit donde agregamos la React.ComponentPropsWithoutRef<C>definición. Sin embargo, debemos recordar omitir accesorios que existen en nuestra definición de componente.
Vamos a encontrar una solución robusta.

Escriba un nuevo tipo que solo comprenda los accesorios que nos gustaría omitir. Es decir, las teclas del AsPropy los accesorios del componente también.

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

¿Recuerdas el keyoftipo de utilidad?

PropsToOmitahora comprenderá un tipo de unión de los accesorios que queremos omitir, que es cada accesorio de nuestro componente representado por Py el accesorio polimórfico real as, representado por AsProps.

Ponga todo esto junto muy bien en la PolymorphicComponentPropdefinición:

type AsProp<C extends React.ElementType> = {
  as?: C;
};

// before 
type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>

// after
type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, 
   PropsToOmit<C, Props>>;

Lo importante aquí es que hemos agregado la siguiente definición:

Omit<React.ComponentPropsWithoutRef<C>, 
   PropsToOmit<C, Props>>;

Esto básicamente omite los tipos correctos de React.componentPropsWithoutRef. ¿Todavía recuerdas cómo omit funciona ?

Por simple que parezca, ¡ahora tiene una solución que puede reutilizar en múltiples componentes en diferentes proyectos!

Aquí está la implementación completa:

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

Ahora podemos continuar y usar PolymorphicComponentPropen nuestro Textcomponente de la siguiente manera:

export const Text = <C extends React.ElementType = "span">({
  as,
  color,
  children,
  // look here 
}: PolymorphicComponentProp<C, TextProps>) => {
  const Component = as || "span";
  const style = color ? { style: { color } } : {};
  return <Component {...style}>{children}</Component>;
};

¡Que agradable! Si crea otro componente, puede continuar y escribirlo así:

PolymorphicComponentProp<C, MyNewComponentProps>

¿Escuchas ese sonido? Ese es el sonido de la victoria: ¡has llegado tan lejos!

Referencias de apoyo en componentes polimórficos

¿Recuerdas todas las referencias React.ComponentPropsWithoutRefhasta ahora?😅Puntales de componentes... sin referencias. Bueno, ¡ahora es el momento de poner los árbitros!

Esta es la parte final y más compleja de nuestra solución. Necesitaré que seas paciente aquí, pero también haré todo lo posible para explicar cada paso en detalle.

Lo primero es lo primero, ¿recuerdas cómo refsfunciona React ? El concepto más importante aquí es que simplemente no pasa refcomo accesorio y espera que se transmita a su componente como cualquier otro accesorio. La forma recomendada de manejar refssus componentes funcionales es usar la forwardReffunción .

Comencemos con una nota práctica.

Si continúa y pasa un refa nuestro Textcomponente ahora, obtendrá un error que dice Property 'ref' does not exist on type ....

// Create the ref object 
const divRef = useRef<HTMLDivElement | null>(null);
... 
// Pass the ref to the rendered Text component
<Text as="div" ref={divRef}>
  Hello Text world
</Text>

Esto se espera.

Nuestra primera oportunidad de admitir referencias será usarlas forwardRefen el Textcomponente como se muestra a continuación:

// before 
export const Text = <C extends React.ElementType = "span">({
  as,
  color,
  children,
}: PolymorphicComponentProp<C, TextProps>) => {
  ...
};


// after
import React from "react";

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">({
    as,
    color,
    children,
  }: PolymorphicComponentProp<C, TextProps>) => {
    ...
  }
);

Esto es esencialmente envolver el código anterior en React.forwardRef, eso es todo.

Ahora, React.forwardReftiene la siguiente firma:

React.forwardRef((props, ref) ... )

Esencialmente, el segundo argumento recibido es el refobjeto. Sigamos adelante y manejemos eso:

type PolymorphicRef<C extends React.ElementType> = unknown;

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: PolymorphicComponentProp<C, TextProps>,
    //  look here
    ref?: PolymorphicRef<C>
  ) => {
    ...
  }
);

Lo que hemos hecho aquí es agregar el segundo argumento, refy declarar su tipo como PolymorphicRef, que solo apunta a unknownpor ahora.

Tenga en cuenta que PolymorphicReftoma en el genérico C. Esto es similar a las soluciones anteriores: el refobjeto de a divdifiere del de a span, por lo que debemos tener en cuenta el tipo de elemento que se pasa a la aspropiedad.

Apunte su atención al PolymorphicReftipo. ¿Cómo podemos obtener el reftipo de objeto en función de la aspropiedad?

Déjame darte una pista: React.ComponentPropsWithRef!

Tenga en cuenta que esto dice con ref. No sin ref.

Esencialmente, si se tratara de un paquete de claves (que, de hecho, lo es), incluirá todos los accesorios de componentes relevantes según el tipo de elemento, además del objeto ref.

Entonces, si sabemos que este tipo de objeto contiene la refclave, también podemos obtener ese tipo de referencia haciendo lo siguiente:

// before 
type PolymorphicRef<C extends React.ElementType> = unknown;

// after 
type PolymorphicRef<C extends React.ElementType> =
  React.ComponentPropsWithRef<C>["ref"];

Esencialmente, React.ComponentPropsWithRef<C>devuelve un tipo de objeto, por ejemplo,

{
  ref: SomeRefDefinition, 
  // ... other keys, 
  color: string 
  href: string 
  // ... etc
}

Para seleccionar solo el reftipo, podemos hacer esto:

React.ComponentPropsWithRef<C>["ref"];

Tenga en cuenta que la sintaxis es similar a la sintaxis del acceso a la propiedad en JavaScript, es decir, ["ref"]. Ahora que tenemos el refaccesorio escrito, podemos continuar y pasarlo al elemento renderizado:

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: PolymorphicComponentProp<C, TextProps>,
    ref?: PolymorphicRef<C>
  ) => {
    //...

    return (
      <Component {...style} ref={ref}> //  look here
        {children}
      </Component>
    );
  }
);

¡Hemos hecho un progreso decente! De hecho, si continúa y verifica el uso de Textcomo lo hicimos antes, no habrá más errores:

// create the ref object 
const divRef = useRef<HTMLDivElement | null>(null);
... 
// pass ref to the rendered Text component
<Text as="div" ref={divRef}>
  Hello Text world
</Text>

Sin embargo, nuestra solución todavía no está tan fuertemente tipada como me gustaría. Avancemos y cambiemos la referencia pasada a Textcomo se muestra a continuación:

// create a "button" ref object 
const buttonRef = useRef<HTMLButtonElement | null>(null);
... 
// pass a button ref to a "div". NB: as = "div"
<Text as="div" ref={buttonRef}>
  Hello Text world
</Text>

TypeScript debería arrojar un error aquí, pero no lo hace. Estamos creando una buttonreferencia, pero pasándola a un divelemento. Eso no está bien.

Si observa el tipo exacto de ref, se ve así:

React.RefAttributes<unknown>.ref?: React.Ref<unknown>

¿Ves el unknownde ahí? Esa es una señal de tipeo débil. Idealmente, deberíamos tener HTMLDivElementallí para definir explícitamente el objeto ref como un divelemento ref.

Tenemos trabajo que hacer. Primero veamos los tipos de los otros accesorios del Textcomponente, que aún hacen referencia al PolymorphicComponentProptipo. Cambie esto a un nuevo tipo llamado PolymorphicComponentPropWithRef. Esto será solo una unión de PolymorphicComponentPropy el ref prop. (Lo adivinaste.)

Aquí está:

type PolymorphicComponentPropWithRef<
  C extends React.ElementType,
  Props = {}
> = PolymorphicComponentProp<C, Props> & 
{ ref?: PolymorphicRef<C> };

Esta es solo una unión de la anterior PolymorphicComponentPropy { ref?: PolymorphicRef<C> }.

Ahora necesitamos cambiar los accesorios del componente para hacer referencia al nuevo PolymorphicComponentPropWithReftipo:

// before
type TextProps = { color?: Rainbow | "black" };

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: PolymorphicComponentProp<C, TextProps>,
    ref?: PolymorphicRef<C>
  ) => {
    ...
  }
);


// now 
type TextProps<C extends React.ElementType> = 
PolymorphicComponentPropWithRef<
  C,
  { color?: Rainbow | "black" }
>;

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: TextProps<C>, //  look here
    ref?: PolymorphicRef<C>
  ) => {
    ...
  }
);

Hemos actualizado TextPropsa la referencia PolymorphicComponentPropWithRefy eso ahora se pasa como accesorios para el Textcomponente. ¡Hermoso!

Queda una última cosa por hacer: proporcione una anotación de tipo para el Textcomponente. Se parece a:

export const Text : TextComponent = ...

TextComponentes el tipo de anotación que escribiremos. Aquí está completamente escrito:

type TextComponent = <C extends React.ElementType = "span">(
  props: TextProps<C>
) => React.ReactElement | null;

Este es esencialmente un componente funcional que toma TextPropsy devuelve React.ReactElement | null, donde TextPropses como se definió anteriormente:

type TextProps<C extends React.ElementType> = 
PolymorphicComponentPropWithRef<
  C,
  { color?: Rainbow | "black" }
>;

¡Con esto, ahora tenemos una solución completa!

Voy a compartir la solución completa ahora. Puede parecer abrumador al principio, pero recuerde que hemos trabajado línea por línea en todo lo que ve aquí. Léalo con esa confianza.

import React from "react";

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

// This is the first reusable type utility we built
type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

// This is a new type utitlity with ref!
type PolymorphicComponentPropWithRef<
  C extends React.ElementType,
  Props = {}
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };

// This is the type for the "ref" only
type PolymorphicRef<C extends React.ElementType> =
  React.ComponentPropsWithRef<C>["ref"];

/**
* This is the updated component props using PolymorphicComponentPropWithRef
*/
type TextProps<C extends React.ElementType> = 
PolymorphicComponentPropWithRef<
  C,
  { color?: Rainbow | "black" }
>;

/**
* This is the type used in the type annotation for the component
*/
type TextComponent = <C extends React.ElementType = "span">(
  props: TextProps<C>
) => React.ReactElement | null;

export const Text: TextComponent = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: TextProps<C>,
    ref?: PolymorphicRef<C>
  ) => {
    const Component = as || "span";

    const style = color ? { style: { color } } : {};

    return (
      <Component {...style} ref={ref}>
        {children}
      </Component>
    );
  }
);

¡Y ahí tienes!

Conclusión e ideas para los próximos pasos.

Ha creado con éxito una solución robusta para manejar componentes polimórficos en React con TypeScript. Sé que no fue un viaje fácil, pero lo lograste.

Gracias por seguirnos.

Esta historia se publicó originalmente en https://blog.logrocket.com/build-strongly-typed-polymorphic-components-react-typescript/

 #typescript #react-native 

Verdie  Murray

Verdie Murray

1636236360

How to add Cypress for Create React App with TypeScript

In this lesson we look at how to add #cypress with code coverage support for a Create #React App application with #TypeScript.

In the end you will have a developer flow that can save you a bunch of time in testing effort 

#react-native #react #cypress #typescript 

Verdie  Murray

Verdie Murray

1638074460

How to Ignore Errors in JSX with TypeScript and React (Example)

This quick lesson demonstrates how to ignore errors in a JSX / #React file with #TypeScript

#typescript #react 

Mathew Rini

1615544450

How to Select and Hire the Best React JS and React Native Developers?

Since March 2020 reached 556 million monthly downloads have increased, It shows that React JS has been steadily growing. React.js also provides a desirable amount of pliancy and efficiency for developing innovative solutions with interactive user interfaces. It’s no surprise that an increasing number of businesses are adopting this technology. How do you select and recruit React.js developers who will propel your project forward? How much does a React developer make? We’ll bring you here all the details you need.

What is React.js?

Facebook built and maintains React.js, an open-source JavaScript library for designing development tools. React.js is used to create single-page applications (SPAs) that can be used in conjunction with React Native to develop native cross-platform apps.

React vs React Native

  • React Native is a platform that uses a collection of mobile-specific components provided by the React kit, while React.js is a JavaScript-based library.
  • React.js and React Native have similar syntax and workflows, but their implementation is quite different.
  • React Native is designed to create native mobile apps that are distinct from those created in Objective-C or Java. React, on the other hand, can be used to develop web apps, hybrid and mobile & desktop applications.
  • React Native, in essence, takes the same conceptual UI cornerstones as standard iOS and Android apps and assembles them using React.js syntax to create a rich mobile experience.

What is the Average React Developer Salary?

In the United States, the average React developer salary is $94,205 a year, or $30-$48 per hour, This is one of the highest among JavaScript developers. The starting salary for junior React.js developers is $60,510 per year, rising to $112,480 for senior roles.

* React.js Developer Salary by Country

  • United States- $120,000
  • Canada - $110,000
  • United Kingdom - $71,820
  • The Netherlands $49,095
  • Spain - $35,423.00
  • France - $44,284
  • Ukraine - $28,990
  • India - $9,843
  • Sweden - $55,173
  • Singapore - $43,801

In context of software developer wage rates, the United States continues to lead. In high-tech cities like San Francisco and New York, average React developer salaries will hit $98K and $114per year, overall.

However, the need for React.js and React Native developer is outpacing local labour markets. As a result, many businesses have difficulty locating and recruiting them locally.

It’s no surprise that for US and European companies looking for professional and budget engineers, offshore regions like India are becoming especially interesting. This area has a large number of app development companies, a good rate with quality, and a good pool of React.js front-end developers.

As per Linkedin, the country’s IT industry employs over a million React specialists. Furthermore, for the same or less money than hiring a React.js programmer locally, you may recruit someone with much expertise and a broader technical stack.

How to Hire React.js Developers?

  • Conduct thorough candidate research, including portfolios and areas of expertise.
  • Before you sit down with your interviewing panel, do some homework.
  • Examine the final outcome and hire the ideal candidate.

Why is React.js Popular?

React is a very strong framework. React.js makes use of a powerful synchronization method known as Virtual DOM, which compares the current page architecture to the expected page architecture and updates the appropriate components as long as the user input.

React is scalable. it utilises a single language, For server-client side, and mobile platform.

React is steady.React.js is completely adaptable, which means it seldom, if ever, updates the user interface. This enables legacy projects to be updated to the most new edition of React.js without having to change the codebase or make a few small changes.

React is adaptable. It can be conveniently paired with various state administrators (e.g., Redux, Flux, Alt or Reflux) and can be used to implement a number of architectural patterns.

Is there a market for React.js programmers?
The need for React.js developers is rising at an unparalleled rate. React.js is currently used by over one million websites around the world. React is used by Fortune 400+ businesses and popular companies such as Facebook, Twitter, Glassdoor and Cloudflare.

Final thoughts:

As you’ve seen, locating and Hire React js Developer and Hire React Native developer is a difficult challenge. You will have less challenges selecting the correct fit for your projects if you identify growing offshore locations (e.g. India) and take into consideration the details above.

If you want to make this process easier, You can visit our website for more, or else to write a email, we’ll help you to finding top rated React.js and React Native developers easier and with strives to create this operation

#hire-react-js-developer #hire-react-native-developer #react #react-native #react-js #hire-react-js-programmer