Volver al blog
TypeScript Types Generics Best Practices
Imagen de portada del artículo: TypeScript Avanzado: Generics, Utility Types y Type Guards

TypeScript Avanzado: Generics, Utility Types y Type Guards

16 min
German

TypeScript es mucho más que añadir tipos a JavaScript. Su sistema de tipos es tan potente que funciona como un lenguaje de programación en sí mismo. Dominar sus características avanzadas te permite escribir código más seguro, expresivo y autodocumentado.

Generics: Código reutilizable con tipos

Los genéricos permiten crear funciones, clases e interfaces que funcionan con cualquier tipo manteniendo la seguridad de tipos.

El problema sin genéricos

typescript
// ❌ Sin genéricos: perdemos información de tipo
function getFirst(arr: any[]): any {
  return arr[0];
}

const num = getFirst([1, 2, 3]);    // tipo: any 😞
const str = getFirst(['a', 'b']);     // tipo: any 😞

// ✅ Con genéricos: el tipo se preserva
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

const num = getFirst([1, 2, 3]);    // tipo: number 🎉
const str = getFirst(['a', 'b']);     // tipo: string 🎉

Restricciones con extends

Puedes limitar qué tipos acepta un genérico:

typescript
// Solo acepta objetos que tengan 'id' y 'name'
interface HasId {
  id: string | number;
}

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

// Funciona con cualquier tipo que tenga 'id'
interface User { id: number; name: string; email: string; }
interface Product { id: string; name: string; price: number; }

const user = findById(users, 1);        // tipo: User | undefined
const product = findById(products, 'abc'); // tipo: Product | undefined

Genéricos en componentes Angular

typescript
// Componente de tabla genérico
@Component({
  selector: 'app-data-table',
  template: `
    <table>
      <thead>
        <tr>
          @for (col of columns(); track col.key) {
            <th>{{ col.label }}</th>
          }
        </tr>
      </thead>
      <tbody>
        @for (row of data(); track trackBy()(row)) {
          <tr>
            @for (col of columns(); track col.key) {
              <td>{{ row[col.key] }}</td>
            }
          </tr>
        }
      </tbody>
    </table>
  `
})
export class DataTableComponent<T extends Record<string, any>> {
  data = input.required<T[]>();
  columns = input.required<{ key: keyof T; label: string }[]>();
  trackBy = input<(item: T) => any>(() => (item: any) => item.id);
}

Utility Types: Los tipos incluidos

TypeScript incluye tipos de utilidad que transforman tipos existentes. Son herramientas esenciales que deberías usar a diario:

Partial<T> — Todas las propiedades opcionales

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

// Perfecto para funciones de actualización parcial
function updateUser(id: string, changes: Partial<User>): void {
  // changes puede tener cualquier combinación de name, email, age
}

updateUser('1', { name: 'German' });           // ✅
updateUser('1', { email: 'a@b.com', age: 28 }); // ✅
updateUser('1', {});                            // ✅

Pick<T, K> y Omit<T, K> — Seleccionar/excluir propiedades

typescript
interface User {
  id: string;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Para la UI no queremos exponer el password
type PublicUser = Omit<User, 'password'>;

// Para un listado solo necesitamos id y name
type UserListItem = Pick<User, 'id' | 'name'>;

// Para crear un usuario no tenemos id ni createdAt
type CreateUserDto = Omit<User, 'id' | 'createdAt'>;

Record<K, V> — Diccionarios tipados

typescript
type UserRole = 'admin' | 'editor' | 'viewer';

interface RolePermissions {
  canEdit: boolean;
  canDelete: boolean;
  canPublish: boolean;
}

// Garantiza que TODOS los roles están definidos
const permissions: Record<UserRole, RolePermissions> = {
  admin:  { canEdit: true, canDelete: true, canPublish: true },
  editor: { canEdit: true, canDelete: false, canPublish: true },
  viewer: { canEdit: false, canDelete: false, canPublish: false },
  // Si olvido un rol, TypeScript me avisa 🎯
};

ReturnType<T> y Parameters<T> — Inferir de funciones

typescript
function createUser(name: string, email: string, role: UserRole) {
  return { id: crypto.randomUUID(), name, email, role, createdAt: new Date() };
}

// Inferir el tipo de retorno sin definirlo manualmente
type User = ReturnType<typeof createUser>;
// { id: string; name: string; email: string; role: UserRole; createdAt: Date }

// Inferir los parámetros
type CreateUserParams = Parameters<typeof createUser>;
// [string, string, UserRole]

Type Guards: Estrechamiento de tipos

Los Type Guards permiten a TypeScript entender qué tipo específico tiene una variable en un punto del código.

Type Guard personalizado con is

typescript
interface Dog { kind: 'dog'; bark(): void; }
interface Cat { kind: 'cat'; meow(): void; }
type Animal = Dog | Cat;

// Type guard con tipo predicado
function isDog(animal: Animal): animal is Dog {
  return animal.kind === 'dog';
}

function handleAnimal(animal: Animal) {
  if (isDog(animal)) {
    animal.bark(); // TypeScript sabe que es Dog aquí
  } else {
    animal.meow(); // TypeScript sabe que es Cat aquí
  }
}

Discriminated Unions (Uniones discriminadas)

Un patrón muy poderoso para manejar estados de forma segura:

typescript
type ApiResponse<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

function handleResponse<T>(response: ApiResponse<T>) {
  switch (response.status) {
    case 'loading':
      return 'Cargando...';
    case 'success':
      return response.data;   // TypeScript sabe que 'data' existe
    case 'error':
      return response.error;  // TypeScript sabe que 'error' existe
  }
}

Template Literal Types

Permiten crear tipos a partir de templates de strings:

typescript
type EventName = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<EventName>}`;
// tipo: 'onClick' | 'onFocus' | 'onBlur'

// Útil para CSS-in-TS
type Size = 'sm' | 'md' | 'lg';
type Breakpoint = 'mobile' | 'tablet' | 'desktop';
type ResponsiveClass = `${Breakpoint}:${Size}`;
// 'mobile:sm' | 'mobile:md' | ... | 'desktop:lg'

Tipos condicionales

Permiten crear tipos que dependen de condiciones:

typescript
// Si T es un array, extraer el tipo de sus elementos
type UnwrapArray<T> = T extends Array<infer U> ? U : T;

type A = UnwrapArray<string[]>;   // string
type B = UnwrapArray<number>;     // number

// Hacer propiedades nullable opcionales
type NullableToOptional<T> = {
  [K in keyof T as null extends T[K] ? K : never]?: T[K];
} & {
  [K in keyof T as null extends T[K] ? never : K]: T[K];
};

Conclusión

TypeScript avanzado es una inversión que se paga sola. Cada tipo bien definido es un bug que nunca llegará a producción. Empieza añadiendo genéricos a tus funciones de utilidad, usa Utility Types para no repetir interfaces, y adopta discriminated unions para manejar estados de forma segura.