TechLead
Lección 6 de 8
5 min de lectura
TypeScript

Genéricos

Crea componentes reutilizables y seguros con tipos genéricos, restricciones y tipos utilitarios

Introducción a los genéricos

Los genéricos te permiten crear componentes reutilizables que funcionan con múltiples tipos manteniendo la seguridad.

// Sin genéricos - se pierde información de tipos
function identityAny(arg: any): any {
  return arg;
}

// Con genéricos - se preserva la información
function identity<T>(arg: T): T {
  return arg;
}

// Uso
const num = identity<number>(42);     // explícito: number
const str = identity("hello");         // inferido: string

// Función flecha genérica
const identityArrow = <T>(arg: T): T => arg;

Interfaces y tipos genéricos

// Interface genérica
interface Box<T> {
  value: T;
  getValue(): T;
}

const numberBox: Box<number> = {
  value: 42,
  getValue() {
    return this.value;
  }
};

// Alias de tipo genérico
type Result<T> = {
  success: boolean;
  data: T;
  error?: string;
};

type UserResult = Result<User>;
type NumberResult = Result<number>;

// Genérico con múltiples parámetros
interface KeyValuePair<K, V> {
  key: K;
  value: V;
}

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

Restricciones genéricas

// Restricción con extends
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("hello");      // OK: string tiene length
logLength([1, 2, 3]);    // OK: array tiene length
// logLength(123);       // Error: number no tiene length

// Restricción con keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = { name: "Alice", age: 30 };
getProperty(person, "name");  // OK: "name" es una key
// getProperty(person, "email"); // Error: "email" no es una key

// Múltiples restricciones
interface Named {
  name: string;
}

interface Aged {
  age: number;
}

function greet<T extends Named & Aged>(entity: T): string {
  return `Hello ${entity.name}, you are ${entity.age} years old`;
}

Clases genéricas

// Clase genérica
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];
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

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

const stringStack = new Stack<string>();
stringStack.push("hello");

// Clase genérica con restricción
class Repository<T extends { id: number }> {
  private items: T[] = [];

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

  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }

  remove(id: number): void {
    this.items = this.items.filter(item => item.id !== id);
  }
}

Tipos utilitarios incorporados

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Partial - todas las propiedades opcionales
type PartialUser = Partial<User>;
const update: PartialUser = { name: "Alice" };

// Required - todas las propiedades requeridas
type RequiredUser = Required<PartialUser>;

// Readonly - todas las propiedades readonly
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = { id: 1, name: "Alice", email: "a@b.com", age: 30 };
// user.name = "Bob"; // Error!

// Pick - selecciona propiedades específicas
type UserPreview = Pick<User, "id" | "name">;

// Omit - excluye propiedades específicas
type UserWithoutEmail = Omit<User, "email">;

// Record - crea un tipo con claves y valores
type UserRoles = Record<string, "admin" | "user" | "guest">;
const roles: UserRoles = {
  alice: "admin",
  bob: "user"
};

// Exclude - excluye tipos de una unión
type Status = "pending" | "active" | "completed" | "cancelled";
type ActiveStatus = Exclude<Status, "cancelled">;  // "pending" | "active" | "completed"

// Extract - extrae tipos de una unión
type OnlyCompleted = Extract<Status, "completed" | "cancelled">;  // "completed" | "cancelled"

// NonNullable - elimina null y undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;  // string

// ReturnType - obtener el tipo de retorno de una función
function createUser() {
  return { id: 1, name: "Alice" };
}
type NewUser = ReturnType<typeof createUser>;  // { id: number; name: string }

// Parameters - obtener tipos de parámetros de una función
function greet(name: string, age: number): void {}
type GreetParams = Parameters<typeof greet>;  // [string, number]

Tipos utilitarios personalizados

// Hacer opcionales propiedades específicas
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type UserWithOptionalEmail = PartialBy<User, "email">;

// Hacer requeridas propiedades específicas
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// Partial profundo (recursivo)
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Tipo nullable
type Nullable<T> = T | null;

// Tipo de elemento de array
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Elem = ArrayElement<string[]>;  // string

// Desempaquetar Promise
type Awaited<T> = T extends Promise<infer U> ? U : T;
type Result = Awaited<Promise<string>>;  // string

Ejemplos prácticos

// Respuesta genérica de API
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: Date;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const data = await response.json();
  return {
    data,
    status: response.status,
    message: "Success",
    timestamp: new Date()
  };
}

// Uso
const userResponse = await fetchData<User>("/api/user");
const usersResponse = await fetchData<User[]>("/api/users");

// Props genéricas para componente React
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Manejador genérico de eventos
type EventHandler<T> = (event: T) => void;

const handleClick: EventHandler<React.MouseEvent> = (e) => {
  console.log(e.clientX, e.clientY);
};

Puntos clave

  • • Los genéricos crean componentes reutilizables y seguros
  • • Usa extends para restringir tipos genéricos
  • • Los tipos utilitarios transforman tipos existentes
  • keyof obtiene las keys de un tipo como unión
  • • Los tipos condicionales permiten lógica a nivel de tipos
  • • Crea utilidades personalizadas para patrones comunes

Continuar Aprendiendo