Skip to content

Genéricos en TypeScript

En este tutorial, aprenderemos sobre los genéricos en TypeScript, una característica poderosa que te permite escribir código más flexible y reutilizable.

¿Qué son los genéricos en TypeScript?

Los genéricos son una forma de crear funciones, clases o interfaces que puedan trabajar con diferentes tipos de datos, sin perder la seguridad de tipos. Es como crear "moldes" que puedes reutilizar para diferentes materiales.

Imagina que tienes una caja, y quieres que esa caja pueda guardar cualquier cosa: zapatos, libros, juguetes, etc. Los genéricos te permiten especificar qué tipo de cosa guardarás, y TypeScript te ayuda a asegurarte de que siempre usas la caja correctamente.

Recursos

Ejemplo Básico

Sin Generics (problemático)

ts
// Función que devuelve lo mismo que recibe
function getData(data: any): any {
  return data;
}

const number = getData(42);
const text = getData("Hello");

// Problema: TypeScript no sabe qué tipo devuelve
// number podría ser string, number, etc.

Con Generics (solución)

ts
// T es como una variable para tipos
function getData<T>(data: T): T {
  return data;
}

// Arrow function con generics
// const getData = <T>(data: T): T => {
//   return data;
// };

const number = getData<number>(42); // number es number
const text = getData<string>("Hello"); // text es string

// O mejor aún, TypeScript puede inferir el tipo:
const number2 = getData(42); // TypeScript sabe que es number
const text2 = getData("Hello"); // TypeScript sabe que es string

Ejemplo con Fetch

ts
// Función genérica para hacer fetch de datos
const fetchData = async <T>(url: string): Promise<T> => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json();
};

const URL_TODO = "https://jsonplaceholder.typicode.com/todos/1";

interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

fetchData<Todo>(URL_TODO)
  .then((todo) => {
    console.log(todo);
  })
  .catch((error) => {
    console.error("Error fetching todo:", error);
  });

const URL_POST = "https://jsonplaceholder.typicode.com/posts/1";

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

fetchData<Post>(URL_POST)
  .then((post) => {
    console.log(post);
  })
  .catch((error) => {
    console.error("Error fetching post:", error);
  });

Ejemplo Fetch con Vue 3

Vue es un framework de JavaScript para crear interfaces de usuario. Se basa en HTML, CSS y JavaScript estándar y proporciona un modelo de programación declarativo basado en componentes que ayuda a desarrollar eficientemente interfaces de usuario de cualquier complejidad.

sh
npm create vite@latest .

src\types\data.type.ts

ts
export interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

export interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

src\lib\fetch-data.ts

ts
export const fetchData = async <T>(url: string): Promise<T> => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Error fetching data: ${response.statusText}`);
  }
  return response.json() as Promise<T>;
};

src\App.vue

vue
<!-- Composition API es una de las características más poderosas de Vue 3, que permite organizar mejor el código y reutilizar la lógica de componentes. En este ejemplo, vamos a utilizar la Composition API junto con TypeScript para crear un componente que obtenga datos de una API y los muestre en la interfaz de usuario. -->
<script setup lang="ts">
import { ref } from "vue";
import { fetchData } from "./lib/fetch-data";
import type { Post, Todo } from "./types/data.type";

// Definición de variables reactivas para almacenar los datos obtenidos
const post = ref<Post | null>(null);
const todo = ref<Todo | null>(null);

const getPost = async () => {
  const data = await fetchData<Post>(
    "https://jsonplaceholder.typicode.com/posts/1"
  );
  // Asignación de los datos obtenidos a la variable reactiva
  post.value = data;
};

const getTodo = async () => {
  const data = await fetchData<Todo>(
    "https://jsonplaceholder.typicode.com/todos/1"
  );
  todo.value = data;
};
</script>

<template>
  <main class="container">
    <h1>Genéricos en TypeScript</h1>
    <div class="d-flex gap-2 mb-2">
      <!-- utilizando directiva @click -->
      <button
        class="btn btn-outline-dark"
        @click="getPost"
      >
        Obtener POST
      </button>
      <button
        class="btn btn-outline-dark"
        @click="getTodo"
      >
        Obtener TODO
      </button>
    </div>

    <!-- utilizando v-if para mostrar los datos obtenidos -->
    <article
      class="card mb-2"
      v-if="post"
    >
      <div class="card-body">
        <!-- utilizando sintaxis de plantilla (interpolación)  -->
        <h2 class="card-tile">{{ post?.title }}</h2>
      </div>
    </article>
    <article
      class="card mb-2"
      v-if="todo"
    >
      <div class="card-body">
        <h2 class="card-tile">{{ todo?.title }}</h2>
      </div>
    </article>
  </main>
</template>

<style scoped></style>

Restricciones con Generics

A veces queremos limitar qué tipos pueden usar nuestros generics:

typescript
// Solo aceptar objetos que tengan una propiedad 'id'
interface HasId {
  id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find((item) => item.id === id);
}

// Funciona con cualquier objeto que tenga 'id'
const users = [
  { id: 1, name: "Ana", email: "ana@email.com" },
  { id: 2, name: "Carlos", email: "carlos@email.com" },
];

const products = [
  { id: 1, name: "Laptop", price: 1200 },
  { id: 2, name: "Mouse", price: 25 },
];

const foundUser = findById(users, 1);
const foundProduct = findById(products, 2);

Ventajas de los Generics

  1. Reutilización: Un mismo código funciona con diferentes tipos
  2. Seguridad: TypeScript verifica los tipos en tiempo de compilación
  3. Autocompletado: El editor sabe qué propiedades y métodos están disponibles
  4. Mantenibilidad: Código más limpio y fácil de mantener

Consejos Prácticos

  1. Nombres descriptivos: Usa T para tipo genérico simple, pero considera nombres más descriptivos como TData, TItem
  2. No abuses: No uses generics si no los necesitas realmente
  3. Restricciones: Usa extends para limitar qué tipos acepta tu generic
  4. Inferencia: Deja que TypeScript infiera los tipos cuando sea posible

¡Los generics son una herramienta poderosa que te ayudará a escribir código más flexible y reutilizable!