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
- Reutilización: Un componente genérico es más flexible y se puede reutilizar con diferentes tipos de datos.
- Seguridad de tipos: TypeScript asegura que los datos pasados al componente sean del tipo esperado, evitando errores en tiempo de ejecución.
- Autocompletado: El uso de genéricos mejora la experiencia del desarrollador, proporcionando mejores sugerencias de código y validación de tipos en editores compatibles con TypeScript.
En resumen, el uso de genéricos en componentes React mejora la flexibilidad y seguridad, permitiendo construir componentes reutilizables y robustos.