Regresar

lectura estimada

Publicado en: 1 feb 2025

Programming

Cambiar idioma:

Entendiendo Genericos en Typescript

Los genéricos son una característica poderosa en TypeScript que te permiten crear componentes reutilizables y flexibles. Permiten que funciones, clases e interfaces trabajen con una variedad de tipos de datos mientras mantienen la seguridad de tipos.

En este artículo, recorreremos los genéricos en TypeScript desde lo más básico hasta ejemplos de nivel medio/avanzado.


¿Por qué usar genéricos?

Primero te voy a mostrar un ejemplo real de un proyecto en el que trabajé hace mucho (pero muuucho) tiempo antes de saber qué eran los genéricos:


type Entity = User | Transaction | Organization;

async function infinite_loader(url: string): Promise<Array<Entity>> {
  //handle the array of models inside of it.
  return entity_arr;
}

const users = await infinite_loader("/api/users?page=2") as Array<User>
const transations = await infinite_loader("/api/transations?page=3") as Array<Transation>
const organizations = await infinite_loader("/api/organizations?page=2") as Array<Organization>

¿Ves el problema con esto?

Esta función se supone que debe ejecutarse en el cliente, y maneja el cargador infinito que llama a la API cada vez que el usuario llega al final de la pantalla.

El problema con esta función es que se supone que debe funcionar para cualquier tipo de modelo que pueda ser solicitado a la API.

Así que si alguien agrega un nuevo modelo al código, entonces tiene que ir al tipo Entity y modificarlo:


type Entity = User | Transaction | Organization | Post | Notification | ThisIsStupid | PleaseStop;

Esto no es escalable, puede parecer fácil simplemente agregar una palabra cada vez que alguien añade un nuevo modelo a la API, pero eso es tedioso y poco inteligente.

Además, tienes que especificar con la palabra clave “as” para obtener el tipo correcto si quieres el autocompletado adecuado.

No vamos a hacer nada de eso, pero supongamos que no nos importa la seguridad de tipos, entonces podríamos hacer algo así:

async function infinite_loader(url: string): Promise<any> {
  //handle the array of models inside of it.
  return entity_arr;
}

Esto funciona, pero perdemos la seguridad de tipos—TypeScript no tiene idea de qué tipo es “entity_arg”, y perdemos características útiles como el autocompletado y la comprobación de errores.

Con genéricos, podemos preservar la información de tipo, así que veamos la función ideal para nuestro primer problema:


async function infinite_loader<T>(url: string): Promise<Array<T>> {
  //handle the array of models inside of it.
  return entity_arr;
}

const users = await infinite_loader<User>("/api/users?page=2")
const transations = await infinite_loader<Transation>("/api/transations?page=3")
const organizations = await infinite_loader<Organization>("/api/organizations?page=2")

¡Ahora sí estamos hablando!

Tenemos el autocompletado funcionando, el código se ve limpio, pero lo más importante: ya no necesitamos agregar nada a este archivo cada vez que alguien de Dev Ops decide trabajar y subir algo nuevo a producción, lo cual, seamos honestos, pasa una vez al año, pero aún así, ya no tenemos que preocuparnos más.


Genéricos Básicos

Función Genérica

function echo<T>(value: T): T {
  return value;
}

const result = echo<string>("Hello TypeScript");

Aquí, “T” es un marcador de posición para el tipo. Puede ser “string”, “number”, “boolean”, un tipo personalizado, etc.

También puedes dejar que TypeScript infiera el tipo:

const result = echo(42); // T is inferred as number

Genéricos con Arrays

Primero, si recuerdas el primer ejemplo, ya viste dos ejemplos diferentes de genéricos:

  Array<string | number | null>// Array themselves contain a generic value.
  Promise<Response>// Async functions return Promises, which also contain generic values.

También puedes hacer funciones genéricas:

function first_element<T>(arr: T[]): T | undefined {
  return arr[0];
}

const first = first_element([1, 2, 3]); // number
const second = first_element(["a", "b"]); // string

Interfaces Genéricas

interface Box<T> {
  value: T;
}

const number_box: Box<number> = { value: 123 };
const string_box: Box<string> = { value: "hello" };

También se pueden pasar multiples parametros:

interface Pair<K, V> {
  key: K;
  value: V;
}

const item: Pair<string, number> = {
  key: "age",
  value: 30,
};

Clases Genéricas

class Container<T> {
  private items: T[] = [];

  add(item: T) {
    this.items.push(item);
  }

  get_all(): T[] {
    return this.items;
  }
}

const string_container = new Container<string>();
string_container.add("hello");

Restricciones con “extends”

¿Qué pasa si queremos asegurarnos de que el tipo genérico tenga una forma específica?

function print_length<T extends { length: number }>(item: T): void {
  console.log(item.length);
}

print_length("Hello"); // Works (string has length)
print_length([1, 2, 3]); // Works
// print_length(123); Error: number doesn't have a length property

Esto nos permite usar genéricos pero asegurarnos de que, si la forma del objeto que intentamos usar no contiene ciertos valores, probablemente estamos usando el objeto equivocado para la función equivocada.


Usando “keyof” con Genéricos

Podemos usar “keyof” para restringir un tipo a un conjunto de claves:

function get_property<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 25 };
const name = get_property(user, "name"); // OK
// get_property(user, "nonexistent"); Error

Tipos Genéricos por Defecto

Puedes proporcionar tipos por defecto para los genéricos:

interface ApiResponse<T = any> {
  data: T;
  success: boolean;
}

const response: ApiResponse<string> = {
  data: "Done",
  success: true,
};

const defaultResponse: ApiResponse = {
  data: { message: "ok" },
  success: true,
};

Avanzado: Tipos Condicionales con Genéricos

Puedes combinar genéricos con tipos condicionales:

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"

O usarlos para crear utilidades:

type NonNullable<T> = T extends null | undefined ? never : T;

type Clean = NonNullable<string | null | undefined>; // string

Ejemplo de mundo real: Un handler genérico de API

interface ApiResponse<T> {
  success: boolean;
  data: T;
}

async function fetch_data<T>(url: string): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url);
    const data = await response.json() as T;
    return {
      success: true,
      data,
    }
  } catch(err) {
    return {
      success: false,
      data : null
    }
  }
}

// Usage
interface User {
  id: number;
  name: string;
}

const user = await fetch_data<User>("/api/user/1");

Conclusión

Los genéricos en TypeScript son esenciales para escribir código limpio, escalable y seguro en cuanto a tipos. Permiten que tus funciones, clases e interfaces trabajen con cualquier tipo de dato manteniendo una tipificación fuerte.

Empieza simple, experimenta seguido e introduce los genéricos gradualmente en tu código real.

Pero lo más importante: Nunca cometas los pecados que yo he cometido.


type Entity = User | Transaction | KillMePlease;

Quizás te interese: