Logo

Uso de Genéricos en Componentes React

En esta guía aprenderemos a tipar componentes para uso genérico potenciado por la inferencia de sus propiedades.

Los genéricos en TypeScript permiten crear componentes y funciones que trabajan con una variedad de tipos, proporcionando una mayor flexibilidad y reutilización de código. En el contexto de React, los genéricos se pueden usar para tipar componentes que manejan datos cuyo tipo puede variar, sin sacrificar la seguridad de los mismos.

Sabemos que los genéricos tienen la capacidad de recibir tipos así como las funciones reciben parámetros. Por defecto, existen genéricos en TypeScript llamados utilidades que nos permiten hacer modificaciones sobre tipos existentes, permitiéndonos agilizar el desarrollo de nuestro código. Una de sus utilidades tiene el nombre de Partial<T>, y puede recibir una interfaz de entrada para dejar opcionales a cada una de sus llaves. Veamos un ejemplo:

interface Username {
  name: string;
  email: string;
}

type PartialUsername = Partial<Username>;

/**
 * PartialUsername {
 *  name?: string;
 *  email?: string;
 * }
 **/

Vemos que el Username original tiene 2 propiedades requeridas, pasándolo por Partial, PartialUsername pasará a tener todas sus propiedades opcionales.

Para más información sobre estas utilidades, en el siguiente link podrán encontrar toda su documentación.

Para la inferencia en TypeScript es necesario que el tipo de una propiedad esté abierta a interpretación del código, por lo tanto, es necesario utilizar genéricos de una manera particular dentro de las funciones. Veamos un ejemplo:

function foo<T>(value: T) {...}

En el ejemplo podemos ver que value no tiene un tipo predefinido, dependerá del valor que le estemos pasando al momento de aplicarlo, es decir, si usamos la función con un valor numérico (1): T será de tipo number y el resto de la función interpretará value como un número y solo será válido usar value como si se tratara de un número, de ahí la seguridad implícita del tipado que nos ofrece TypeScript.

Componentes React

En los componentes de React podemos hacer lo mismo con sus propiedades; sin embargo, puede ser algo confuso al manejar JSX, debido a su etiquetado HTML, por lo que para pasarle genéricos debemos hacerlo un poco diferente. Veamos un ejemplo:

interface Props<T> {
  value: T;
}

function Component<T>({ value }: Props<T>) {
  return <p>{typeof value}</p>;
}

En esta primera parte del ejemplo estamos tipando el componente como una función normal; veamos la siguiente parte:

// ...

function Layout() {
  return <Component<number> value={2} />; // ✅ 2 es valido porque se trata de un valor númerico
}

Hasta aquí todo bien, pero si intentáramos predefinir el tipo T a un tipo distinto a number nos saldría un error de tipado. Veamos un ejemplo:

// ...

function Layout() {
  return <Component<string> value={2} />; // ❌ 2 es inválido ya que no es una cadena de texto
}

// Type 'number' is not assignable to type 'string'.ts(2322)

Encontramos útil el ejemplo para entender cómo funciona el tipado en componentes React; sin embargo, es poco usual encontrarse código de esa forma, ya que la inferencia hace todo el trabajo. Vamos a lo que realmente nos interesa: una aplicación real con la que logremos explotar el verdadero potencial de su uso:

interface Props<T extends Record<string, string>> {
  data: T;
  primaryKey: keyof T;
}

function Component<T extends Record<string, string>>({ data, primaryKey }: Props<T>) {
  return <p>{data[primaryKey]}</p>;
}

function Layout() {
  return <Component data={{ id: '1', name: 'John' }} primaryKey="id" />;
}

En el anterior ejemplo, la propiedad primaryKey por inferencia del objeto data solo podrá recibir un nombre de las llaves definidas en ese objeto, es decir, id o name. Esto es sumamente potente si en el componente debemos realizar operaciones alrededor de la llave primaria (primaryKey). También es posible utilizarlo en arreglos y poder iterar sobre cada uno de los objetos del arreglo utilizando la llave primaria, y seleccionando la llave a renderizar de la lista, veamos otro ejemplo:

interface Props<T extends Record<string, string>> {
  data: T[];
  primaryKey: keyof T;
  renderItem: (item: T) => React.ReactNode;
}

function Component<T extends Record<string, string>>({ data, primaryKey }: Props<T>) {
  return (
    <ul>
      {data.map((item) => (
        <li key={item[primaryKey]}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

function Layout() {
  return (
    <Component
      data={[{ id: '1', name: 'John' }]}
      primaryKey="id"
      renderItem={({ name }) => <span>{name}</span>}
    />;
  );
}

Vemos que primaryKey lo usamos para asignarle un identificador único a la propiedad key de la etiqueta <li ...> y la nueva propiedad renderItem la usamos para seleccionar de las llaves del objeto cuál renderizar. Lo potente de todo esto lleva a que en el editor podamos ver las llaves disponibles con la ayuda del autocompletado.

Beneficios

En resumen, el uso de genéricos en componentes React mejora la flexibilidad y seguridad, permitiendo construir componentes reutilizables y robustos.


reacttypescriptgenericosinferencias