🟦 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.
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"); }
💡
anydesactiva el compilador para ese valor.unknownes su primo seguro: cualquier cosa puede serunknown, 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
interfacepara objetos públicos ytypepara 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)
💡
extendsen 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.inferes 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 constcongela literales:const env = { e: "prod" } as consthaceede tipo"prod", nostring. Ú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
- TypeScript Handbook — The Type System: la referencia oficial sobre utility, conditional y mapped types.
- TypeScript Handbook — Generics: identidad, constraints e infer en profundidad.
- type-challenges: ejercicios avanzados del sistema de tipos, de fácil a diabólico.
- Total TypeScript: tutoriales y la “TypeScript Tips” newsletter de Matt Pocock.
- type-fest: utility types que la librería estándar no incluye.
- Ruta completa: Backend con TypeScript.