🔎 Buscar

🟦 El sistema de tipos de TypeScript

Tipos básicos, union e intersection, generics, narrowing, utility types, conditional y mapped types con ejemplos reales y ejercicios.

Wiki / Apuntes📖 Contenido

El sistema de tipos de TypeScript

TypeScript es JavaScript con un sistema de tipos estructural y estático que corre en tiempo de compilación. La diferencia con los tipos nominales de Java o C#: aquí lo que importa es la forma del valor, no su nombre. Dominar los tipos no es “escribir más anotaciones”: es dejar que el compilador elimine una categoría entera de bugs antes de que lleguen a producción.

Tipos básicos y literales

const nombre: string = "Ana";
const edad: number = 30;
const activo: boolean = true;
const ids: number[] = [1, 2, 3];
const valor: any = "cualquier cosa"; // ⚠️ evita: desactiva el checker

// Tipos literales: el tipo ES un valor concreto
let estado: "activo" | "inactivo" = "activo";
estado = "borrado"; // ❌ Error: "borrado" no es un valor del tipo

// Tuplas
const punto: [number, number] = [10, 20];

// null / undefined / void / never
function nada(): void {}            // no devuelve nada útil
function noRegresa(): never { throw new Error("siempre lanza"); }

💡 any desactiva el compilador para ese valor. unknown es su primo seguro: cualquier cosa puede ser unknown, pero no puedes usarlo sin reducirlo antes (ver narrowing).

Union e intersection types

La union (|) dice “este valor es de al menos uno de estos tipos”. La intersection (&) dice “este valor tiene todos estos tipos a la vez”.

type Id = string | number;               // acepta ambos

function buscar(id: Id) {
  // aún no sabes cuál es: hace falta narrowing para usarlo
  if (typeof id === "string") {
    return id.toUpperCase();             // id es string aquí
  }
  return id.toFixed(0);                  // id es number aquí
}

// Intersection: combina formas
type ConId = { id: number };
type ConNombre = { nombre: string };
type Usuario = ConId & ConNombre;        // { id: number; nombre: string }

const u: Usuario = { id: 1, nombre: "Ana" };

type vs interface

Ambos definen formas de objetos; la diferencia práctica es de capacidad:

type interface
Uniones / intersecciones
Tuplas ❌ (con sintaxis de función)
Mapped / conditional types
Declaración merging (reabrir)
Extender & extends
// type: flexible, para casi todo
type Respuesta = { ok: true; data: string } | { ok: false; error: string };

// interface: ideal para objetos públicos y clases (extends + merging)
interface Animal { nombre: string }
interface Perro extends Animal { ladra(): void }

// El mismo nombre se puede "reabrir" con interface
interface Window { version: string }

💡 Regla pragmática de la comunidad: usa interface para objetos públicos y type para uniones, tuplas, y tipos que dependen de otros tipos. El compilador los trata casi igual.

Generics: tipos parametrizados

Un generic es una función/ tipo que recibe un tipo como parámetro y lo usa internamente:

// Identidad: devuelve el MISMO tipo que recibe
function identidad<T>(valor: T): T {
  return valor;
}
const s = identidad("hola");   // s: string
const n = identidad(42);       // n: number

// Constraints: T debe tener cierta forma
function largo<T extends { length: number }>(item: T): number {
  return item.length;          // seguro: T garantiza length
}
largo([1, 2, 3]);              // ✅
largo("abc");                  // ✅ (string tiene length)
largo(42);                     // ❌ number no tiene length

// Infer: el compilador deduce los tipos por ti
function par<T, U>(a: T, b: U): [T, U] { return [a, b]; }
const p = par("x", 3);         // p: [string, number] (inferido, sin anotar)

💡 extends en generics es constraint, no herencia. T extends { length: number } significa “T debe ser asignable a eso”, no que T herede de una clase.

Narrowing: reducir un tipo

Narrowing es convertir un tipo general (union o unknown) en un tipo concreto dentro de un bloque:

function procesar(valor: unknown) {
  if (typeof valor === "string") {
    return valor.toUpperCase();        // narrowed a string
  }
  if (Array.isArray(valor)) {
    return valor.length;               // narrowed a any[]
  }
  return 0;
}

// instanceof: distingue instancias de clases
class Error404 extends Error {}
function manejar(e: Error) {
  if (e instanceof Error404) return "no encontrado";
  return "otro error";
}

// Discriminated unions: un campo "discriminador" te dice cuál es
type Pedido = 
  | { tipo: "creado";   id: number }
  | { tipo: "pagado";   id: number; monto: number }
  | { tipo: "cancelado"; id: number; motivo: string };

function resumen(p: Pedido) {
  switch (p.tipo) {
    case "creado":     return `Pedido ${p.id} creado`;
    case "pagado":     return `Pagado ${p.monto}`;
    case "cancelado":  return `Cancelado: ${p.motivo}`;
  }
}

Utility types

TypeScript incluye helpers que transforman tipos a partir de otros:

Utility Qué hace Ejemplo
Partial<T> Todas las props opcionales Partial<Config>
Required<T> Todas las props requeridas Required<Config>
Readonly<T> Todas las props de solo lectura Readonly<Usuario>
Pick<T, K> Solo las props K Pick<Usuario, "id" | "nombre">
Omit<T, K> Todas menos las K Omit<Usuario, "password">
Record<K, V> Objeto con claves K y valores V Record<"a" | "b", number>
Exclude<T, U> Quita U de T (union) Exclude<"a" | "b" | "c", "b">
Extract<T, U> Se queda solo con U Extract<...>
ReturnType<F> Tipo de retorno de F ReturnType<typeof fn>
Parameters<F> Tupla de parámetros de F Parameters<typeof fn>
Awaited<T> Desenvuelve promesas Awaited<Promise<string>> = string
interface Usuario {
  id: number;
  nombre: string;
  password: string;
}

// No expongas el password en la respuesta pública
type UsuarioPublico = Omit<Usuario, "password">;
// { id: number; nombre: string }

// Un patch parcial en una actualización
type Patch = Partial<Usuario>;

// Un mapa de estado a handler
const handlers: Record<"crear" | "editar" | "borrar", () => void> = {
  crear: () => {},
  editar: () => {},
  borrar: () => {},
};

function creaUsuario(): Promise<Usuario> { /* ... */ }
type Res = Awaited<ReturnType<typeof creaUsuario>>;  // Usuario

Conditional types e infer

Un conditional type elige un tipo según una condición evaluada en tiempo de compilación:

type EsString<T> = T extends string ? "sí" : "no";

type A = EsString<"hola">;    // "sí"
type B = EsString<number>;    // "no"

// infer: extrae un tipo desde dentro de otro
type ElementoDeArray<T> = T extends (infer U)[] ? U : never;

type E1 = ElementoDeArray<number[]>;   // number
type E2 = ElementoDeArray<string[]>;   // string

type Desempaquetar<T> = T extends Promise<infer U> ? U : T;
type X = Desempaquetar<Promise<number>>;  // number
type Y = Desempaquetar<string>;           // string

💡 ReturnType<T> se implementa así: T extends (...args: any) => infer R ? R : any. infer es la “variable” que el compilador deduce del patrón. Es la pieza que hace posibles las librerías de tipos avanzadas (zod, type-fest, trpc).

Mapped types: transformar objeto a objeto

Iteran sobre las claves de un tipo y generan otro:

type SoloLectura<T> = { readonly [K in keyof T]: T[K] };
type Opcional<T> = { [K in keyof T]?: T[K] };

interface Config { host: string; puerto: number }
type ConfigRo = SoloLectura<Config>;   // todas readonly
type ConfigPar = Opcional<Config>;     // todas opcionales

// keyof + in: filtrar o renombrar claves
type SoloString<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K] };

interface Mixto { a: string; b: number; c: string }
type SoloCadenas = SoloString<Mixto>;  // { a: string; c: string }

satisfes (as const + satisfies)

satisfies comprueba que un valor cumple un tipo sin cambiar su tipo literal: lo mejor de ambos mundos (validación estricta + tipo preciso):

const config = {
  puerto: 3000,
  env: "production",
} satisfies Record<string, string | number>;

config.puerto;        // ✅ tiene tipo number (no se ensancha)
config.env;           // ✅ tipo "production" (literal conservado)
// config.inexistente; // ❌ error: no existe en el tipo

Sin satisfies, con const config: Record<...> = {...} perderías el tipo literal de env y la verificación de propiedades correctas.

⚠️ as const congela literales: const env = { e: "prod" } as const hace e de tipo "prod", no string. Útil para configs y const enrutadas, pero readonly — no lo uses para datos mutables.

Ejercicios con ejemplos reales

// 1) Función que procesa una respuesta de API (puede fallar)
type ApiResultado<T> =
  | { estado: "exito";   data: T }
  | { estado: "error";   mensaje: string };

function parsear<T>(raw: string): ApiResultado<T> {
  try {
    return { estado: "exito", data: JSON.parse(raw) as T };
  } catch (e) {
    return { estado: "error", mensaje: (e as Error).message };
  }
}

// 2) Extraer el tipo de un array de una respuesta anidada
type RespuestaApi<T> = { items: T[]; total: number };
type ItemsDe<T> = T extends RespuestaApi<infer U> ? U : never;
type X = ItemsDe<RespuestaApi<string>>;   // string

// 3) Un mapa tipado de endpoints a parámetros
type RutaParams = {
  "/usuario/:id": { id: string };
  "/pedidos": Record<string, never>;
};
type ParamsDe<R extends keyof RutaParams> = RutaParams[R];
const p: ParamsDe<"/usuario/:id"> = { id: "42" };  // ✅ validado

// 4) Unión discriminada + exhaustividad con never
type Evento =
  | { tipo: "abrir"; ventana: string }
  | { tipo: "cerrar" };

function manejador(e: Evento): void {
  switch (e.tipo) {
    case "abrir":  console.log("abre", e.ventana); break;
    case "cerrar": console.log("cierra");          break;
    default:
      const _exhaustivo: never = e;   // si añades un Evento nuevo,
      break;                          // el compilador exige el caso
  }
}

Cheatsheet

Quieres… Usas…
“Acepto string o number” string | number
“Todos a la vez” A & B
“Describo sin anotar” const t = <T>(x: T): T => x (infer)
“Filtrar el tipo en un bloque” typeof, instanceof, in, switch de discriminante
“Props opcionales” Partial<T>
“Solo estas props” Pick<T, K> / Omit<T, K>
“Objeto de claves a valores” Record<K, V>
“Tipo de retorno de una fn” ReturnType<typeof fn>
“Extraer tipo de adentro” T extends (infer U)[] ? U : never
“Transformar cada clave” mapped types { [K in keyof T]: ... }
“Validar manteniendo literales” satisfies
“Exhaustividad” union discriminada + never en el default

Para profundizar

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.