DevToolBoxGRATIS
Blog

TypeScript Generics explicados: Guía práctica con ejemplos

14 min de lecturapor DevToolBox

Los genericos de TypeScript son una de las caracteristicas mas poderosas del sistema de tipos. Te permiten escribir codigo flexible, reutilizable y seguro en tipos parametrizando tipos. En lugar de duplicar funciones o clases para cada tipo de dato, los genericos te permiten escribir una vez y usar con cualquier tipo manteniendo la seguridad de tipos completa. Esta guia cubre todo, desde lo basico hasta patrones avanzados.

Tu Primera Funcion Generica

El punto de partida clasico es la funcion identidad — una funcion que retorna exactamente lo que recibe. Sin genericos, perderas informacion de tipo usando any, o tendras que escribir funciones separadas para cada tipo.

Con genericos, defines un parametro de tipo (convencionalmente llamado T) que actua como marcador de posicion. Al llamar la funcion, TypeScript infiere el tipo real automaticamente.

// Without generics — loses type info
function identityAny(value: any): any {
  return value;
}
const result1 = identityAny("hello"); // type: any (not string!)

// With generics — preserves type info
function identity<T>(value: T): T {
  return value;
}
const result2 = identity("hello");    // type: string ✓
const result3 = identity(42);         // type: number ✓
const result4 = identity(true);       // type: boolean ✓

// Explicit type argument (rarely needed)
const result5 = identity<string>("hello"); // type: string

Funciones Genericas

Las funciones genericas pueden aceptar multiples parametros de tipo e incluso tener tipos predeterminados. Esto las hace increiblemente versatiles.

// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}
const p = pair("hello", 42); // type: [string, number]

// Swap function with two type params
function swap<A, B>(tuple: [A, B]): [B, A] {
  return [tuple[1], tuple[0]];
}
const swapped = swap(["hello", 42]); // type: [number, string]

// Default type parameters
function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}
const strings = createArray(3, "x");   // T inferred as string
const numbers = createArray(3, 0);     // T inferred as number
const defaults = createArray<>(3, ""); // T defaults to string

Los parametros de tipo predeterminados funcionan como los parametros de funcion predeterminados — si el llamador no especifica un tipo, se usa el predeterminado.

Interfaces y Tipos Genericos

Los genericos no se limitan a funciones. Puedes parametrizar interfaces y alias de tipo para crear estructuras de datos flexibles.

// Generic interface
interface Box<T> {
  value: T;
  label: string;
}

const stringBox: Box<string> = { value: "hello", label: "greeting" };
const numberBox: Box<number> = { value: 42, label: "answer" };

// Generic interface with multiple params
interface KeyValuePair<K, V> {
  key: K;
  value: V;
}

// Generic type alias
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

const ok: Result<string> = { success: true, data: "hello" };
const fail: Result<string> = { success: false, error: new Error("oops") };

// Generic type for callbacks
type AsyncCallback<T> = (error: Error | null, data: T | null) => void;

// Generic type for nullable values
type Nullable<T> = T | null | undefined;

Las interfaces genericas son especialmente utiles para definir respuestas de API, colecciones y contenedores de datos.

Clases Genericas

Las clases tambien pueden ser genericas. Esto es util para construir estructuras de datos como pilas, colas, listas enlazadas o cualquier contenedor.

// Generic Stack class
class Stack<T> {
  private items: T[] = [];

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

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // type: number | undefined

const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.peek(); // type: string | undefined

// Generic class with multiple type params
class HashMap<K, V> {
  private map = new Map<K, V>();

  set(key: K, value: V): void {
    this.map.set(key, value);
  }

  get(key: K): V | undefined {
    return this.map.get(key);
  }

  has(key: K): boolean {
    return this.map.has(key);
  }
}

El parametro de tipo esta disponible en toda la clase — en propiedades, parametros de metodo y tipos de retorno.

Restricciones Genericas con extends

Los genericos sin restriccion aceptan cualquier tipo, pero a veces necesitas limitar los tipos permitidos. La palabra clave extends permite agregar restricciones.

Restringir a Claves de Objeto con keyof

Un patron muy comun es restringir un parametro de tipo a ser una clave valida de otro tipo.

// getProperty with keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30, email: "alice@example.com" };

getProperty(user, "name");  // type: string ✓
getProperty(user, "age");   // type: number ✓
// getProperty(user, "foo"); // Error: "foo" is not assignable to "name" | "age" | "email"

// Pick specific keys from an object
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => { result[key] = obj[key]; });
  return result;
}

const subset = pick(user, ["name", "email"]);
// type: { name: string; email: string }

Restringir a Formas Especificas

Puedes requerir que un parametro de tipo tenga ciertas propiedades extendiendo una interfaz o tipo objeto.

// Constraint to objects with a length property
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(value: T): T {
  console.log(`Length: ${value.length}`);
  return value;
}

logLength("hello");     // OK — string has length
logLength([1, 2, 3]);   // OK — array has length
logLength({ length: 5, name: "test" }); // OK — has length
// logLength(42);       // Error — number has no length property

// Constraint to objects with an id
interface HasId {
  id: number | string;
}

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

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

const found = findById(users, 1);
// type: { id: number; name: string } | undefined

Tipos Condicionales

Los tipos condicionales permiten elegir entre dos tipos basandose en una condicion, usando la sintaxis T extends U ? X : Y.

// Basic conditional type
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true

// Flatten arrays, leave other types alone
type Flatten<T> = T extends Array<infer Item> ? Item : T;

type D = Flatten<string[]>;    // string
type E = Flatten<number[][]>;  // number[]
type F = Flatten<string>;      // string (not an array, returned as-is)

// Extract return type (simplified ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type G = MyReturnType<() => string>;           // string
type H = MyReturnType<(x: number) => boolean>; // boolean

// Extract promise value
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type I = UnwrapPromise<Promise<string>>; // string
type J = UnwrapPromise<number>;          // number

Los tipos condicionales se vuelven especialmente poderosos combinados con infer, que permite extraer tipos de otros tipos.

Tipos Mapeados

Los tipos mapeados transforman las propiedades de un tipo existente para crear uno nuevo. Iteran sobre claves usando la sintaxis [K in keyof T].

// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties required (like Required<T>)
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

// Transform property types
type Stringify<T> = {
  [K in keyof T]: string;
};

interface User {
  name: string;
  age: number;
  active: boolean;
}

type StringifiedUser = Stringify<User>;
// { name: string; age: string; active: string }

// Mapped type with conditional
type OptionalNullable<T> = {
  [K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};

Puedes agregar o eliminar modificadores como readonly y ? en tipos mapeados.

Tipos de Utilidad Incorporados

TypeScript incluye muchos tipos de utilidad construidos con genericos. Aqui estan los mas utilizados:

Tipo de UtilidadDescripcionEjemplo
Partial<T>Hace todas las propiedades opcionalesPartial<User>
Required<T>Hace todas las propiedades obligatoriasRequired<Partial<User>>
Pick<T, K>Selecciona las propiedades especificadasPick<User, "name" | "email">
Omit<T, K>Excluye las propiedades especificadasOmit<User, "password">
Record<K, V>Crea un mapeo de tipo clave-valorRecord<string, number>
Readonly<T>Hace todas las propiedades de solo lecturaReadonly<Config>
ReturnType<T>Extrae el tipo de retorno de una funcionReturnType<typeof fn>
Parameters<T>Extrae la tupla de tipos de parametrosParameters<typeof fn>
// Utility types in action
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// For update forms — all fields optional
type UpdateUser = Partial<User>;

// Public user — omit password
type PublicUser = Omit<User, "password">;

// User creation — omit auto-generated fields
type CreateUser = Omit<User, "id" | "createdAt">;

// Lookup table
type UserLookup = Record<number, User>;

// Immutable user from API
type FrozenUser = Readonly<User>;

// Extract function types
function fetchUser(id: number): Promise<User> { /* ... */ }
type FetchReturn = ReturnType<typeof fetchUser>;     // Promise<User>
type FetchParams = Parameters<typeof fetchUser>;     // [number]

Patrones del Mundo Real

Wrapper de Respuesta API Generico

Uno de los usos mas comunes de genericos es envolver respuestas de API en una estructura consistente.

// Generic API response wrapper
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number;
    pageSize: number;
    totalItems: number;
    totalPages: number;
  };
}

// Usage with specific types
interface User {
  id: number;
  name: string;
  email: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return res.json();
}

// TypeScript knows the exact data type
const userRes = await fetchApi<User>("/api/users/1");
console.log(userRes.data.name); // type: string ✓

const productsRes = await fetchApi<Product[]>("/api/products");
console.log(productsRes.data[0].price); // type: number ✓

Props de Componente React Generico

Los genericos permiten construir componentes React que funcionan con diferentes tipos de datos manteniendo la seguridad de tipos.

// Generic list component props
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  onItemClick?: (item: T) => void;
  emptyMessage?: string;
}

function GenericList<T>({
  items,
  renderItem,
  keyExtractor,
  onItemClick,
  emptyMessage = "No items",
}: ListProps<T>) {
  if (items.length === 0) return <p>{emptyMessage}</p>;
  return (
    <ul>
      {items.map((item, index) => (
        <li
          key={keyExtractor(item)}
          onClick={() => onItemClick?.(item)}
        >
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

// Usage — TypeScript infers T from items
<GenericList
  items={users}
  keyExtractor={(user) => user.id}
  renderItem={(user) => <span>{user.name}</span>}
  onItemClick={(user) => console.log(user.email)}
/>

Patron Repository Generico

El patron repository abstrae el acceso a datos detras de una interfaz generica, permitiendo cambiar fuentes de datos sin modificar la logica de negocio.

// Generic repository interface
interface Entity {
  id: number | string;
}

interface Repository<T extends Entity> {
  findAll(): Promise<T[]>;
  findById(id: T["id"]): Promise<T | null>;
  create(data: Omit<T, "id">): Promise<T>;
  update(id: T["id"], data: Partial<T>): Promise<T>;
  delete(id: T["id"]): Promise<boolean>;
}

// Concrete implementation
class ApiRepository<T extends Entity> implements Repository<T> {
  constructor(private baseUrl: string) {}

  async findAll(): Promise<T[]> {
    const res = await fetch(this.baseUrl);
    return res.json();
  }

  async findById(id: T["id"]): Promise<T | null> {
    const res = await fetch(`${this.baseUrl}/${id}`);
    if (!res.ok) return null;
    return res.json();
  }

  async create(data: Omit<T, "id">): Promise<T> {
    const res = await fetch(this.baseUrl, {
      method: "POST",
      body: JSON.stringify(data),
    });
    return res.json();
  }

  async update(id: T["id"], data: Partial<T>): Promise<T> {
    const res = await fetch(`${this.baseUrl}/${id}`, {
      method: "PATCH",
      body: JSON.stringify(data),
    });
    return res.json();
  }

  async delete(id: T["id"]): Promise<boolean> {
    const res = await fetch(`${this.baseUrl}/${id}`, { method: "DELETE" });
    return res.ok;
  }
}

// Usage
interface User extends Entity { id: number; name: string; email: string; }
const userRepo = new ApiRepository<User>("/api/users");
const users = await userRepo.findAll(); // type: User[]

Emisor de Eventos Generico

Un emisor de eventos type-safe garantiza que los nombres de eventos y sus payloads esten correctamente tipados.

// Type-safe event emitter
type EventMap = Record<string, any>;
type EventCallback<T> = (payload: T) => void;

class TypedEmitter<Events extends EventMap> {
  private listeners: {
    [K in keyof Events]?: EventCallback<Events[K]>[];
  } = {};

  on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(callback);
  }

  off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
    const cbs = this.listeners[event];
    if (cbs) {
      this.listeners[event] = cbs.filter(cb => cb !== callback);
    }
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    const cbs = this.listeners[event];
    if (cbs) {
      cbs.forEach(cb => cb(payload));
    }
  }
}

// Define your event types
interface AppEvents {
  "user:login": { userId: string; timestamp: number };
  "user:logout": { userId: string };
  "item:added": { itemId: string; quantity: number };
  "error": { message: string; code: number };
}

const emitter = new TypedEmitter<AppEvents>();

// Fully typed — payload type is inferred
emitter.on("user:login", (payload) => {
  console.log(payload.userId);    // type: string ✓
  console.log(payload.timestamp); // type: number ✓
});

// Error: "unknown:event" is not assignable to keyof AppEvents
// emitter.on("unknown:event", () => {});

Errores Comunes

Aqui estan los errores mas frecuentes con genericos y sus correcciones:

ErrorProblemaCorreccion
Usar any en lugar de genericosPierde toda la informacion de tipoUsar parametro de tipo T en lugar de any
Sobre-restringir parametros de tipoHace el generico inutilizable para tipos validosSolo restringir lo que realmente necesitas
Parametros de tipo innecesariosAgregar T usado solo una vez aumenta la complejidadSi T solo aparece en el tipo de retorno, usar el tipo concreto
No usar restricciones al acceder a propiedadesError TypeScript: la propiedad no existe en el tipo TAgregar restriccion extends: T extends { prop: type }
Confundir valores predeterminados con restriccionesEl tipo predeterminado no restringe — los llamadores pueden pasar otros tiposUsar extends para restringir, = para predeterminado
No inferir cuando es posiblePasar tipos explicitamente cuando TS puede inferirlosDejar que TypeScript infiera tipos de los argumentos
Usar T[] en lugar de readonly T[]Permite mutacion de arrays que deberian ser solo lecturaUsar readonly T[] para datos inmutables de APIs
// ❌ Mistake: Using any
function badParse(json: string): any {
  return JSON.parse(json);
}
const data = badParse('{"name":"Alice"}');
data.whatever; // No error — but might crash at runtime!

// ✓ Correction: Using generics with validation
function safeParse<T>(json: string, validator: (data: unknown) => data is T): T | null {
  try {
    const parsed = JSON.parse(json);
    return validator(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

// ❌ Mistake: Unnecessary type parameter
function badLength<T>(arr: T[]): number {
  return arr.length; // T is never meaningfully used
}

// ✓ Correction: Just use the concrete type
function goodLength(arr: unknown[]): number {
  return arr.length;
}

// ❌ Mistake: Missing constraint
function badGetName<T>(obj: T): string {
  return obj.name; // Error: Property 'name' does not exist on type 'T'
}

// ✓ Correction: Add constraint
function goodGetName<T extends { name: string }>(obj: T): string {
  return obj.name; // OK ✓
}

Preguntas Frecuentes

Cual es la diferencia entre genericos y el tipo any?

Los genericos preservan la informacion de tipo mientras que any la descarta. Con genericos, si pasas un string, TypeScript sabe que el retorno tambien es string. Con any, toda la informacion se pierde.

Cuando usar genericos vs sobrecargas de funciones?

Usa genericos cuando la logica es la misma para todos los tipos. Usa sobrecargas cuando la implementacion difiere segun el tipo de entrada.

Se pueden usar genericos con funciones flecha en archivos TSX?

Si, pero agrega una coma final para evitar ambiguedad JSX: const fn = <T,>(arg: T): T => arg.

Cuantos parametros de tipo deberia tener un generico?

Los menos posibles. Uno o dos es comun. Tres a veces es necesario. Mas de tres, considera refactorizar.

Los genericos afectan el rendimiento en tiempo de ejecucion?

No. Los genericos son puramente una caracteristica de tiempo de compilacion. Se eliminan completamente durante la compilacion sin overhead en tiempo de ejecucion.

Cual es la convencion de nomenclatura para parametros de tipo?

Las letras mayusculas individuales son convencionales: T para tipo, K para clave, V para valor, E para elemento, R para tipo de retorno.

𝕏 Twitterin LinkedIn
¿Fue útil?

Mantente actualizado

Recibe consejos de desarrollo y nuevas herramientas.

Sin spam. Cancela cuando quieras.

Prueba estas herramientas relacionadas

TSJSON to TypeScriptJSTypeScript to JavaScriptGTGraphQL to TypeScript

Artículos relacionados

JSON a TypeScript: Guia completa con ejemplos

Aprende a convertir datos JSON a interfaces TypeScript automáticamente. Objetos anidados, arrays, campos opcionales y mejores prácticas.

TypeScript vs JavaScript: Cuándo y cómo convertir

Guía práctica sobre cuándo convertir TypeScript a JavaScript y viceversa. Estrategias de migración, herramientas, impacto en el tamaño del bundle y consideraciones de equipo.

JSON a Zod Schema: Validacion en Tiempo de Ejecucion con Tipos Seguros en TypeScript

Aprende a convertir JSON a schemas Zod para validacion en tiempo de ejecucion con tipos seguros en TypeScript.

GraphQL a TypeScript: Guia de generacion de codigo

Genere tipos TypeScript desde schemas GraphQL.