TypeScript no es solo "JavaScript con tipos". Es un lenguaje de tipos Turing completo que te permite codificar invariantes del dominio en tiempo de compilación. Cuando dominas su sistema de tipos, los bugs que llegaban a producción se convierten en errores de compilación.
Más allá de los tipos básicos
El sistema de tipos de TypeScript es estructural (duck typing), no nominal como Java o C#. Dos objetos con la misma forma son del mismo tipo, aunque no tengan una interfaz compartida:
interface Point { x: number; y: number }
interface Coordinate { x: number; y: number }
const p: Point = { x: 1, y: 2 }
const c: Coordinate = p // ✅ compatible estructuralmenteTipos condicionales
Permiten elegir un tipo basado en una condición. Son el equivalente a un ternario en el nivel de tipos:
type IsString<T> = T extends string ? "sí" : "no"
type A = IsString<"hola"> // "sí"
type B = IsString<42> // "no"Template literal types
Transforman strings en el sistema de tipos. Útiles para eventos, rutas, o builders:
type EventName = `on${Capitalize<string>}`
type Route = `/api/${string}/:id`
type ApiRoute = `/api/${"users" | "posts"}/${number}`
// "/api/users/42" ✅
// "/api/posts/7" ✅
// "/api/mal/xyz" ❌Mapped types con as
Puedes re-mapear claves de un objeto:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type User = { name: string; age: number }
type UserGetters = Getters<User>
// { getName: () => string; getAge: () => number }Uniones discriminadas como máquinas de estado
Es el patrón más poderoso de TypeScript. Modela cada estado como una variante con un discriminante común (type o kind):
type ApiState<T> =
| { type: "idle" }
| { type: "loading" }
| { type: "success"; data: T; timestamp: number }
| { type: "error"; error: Error; code: number; retry: boolean }
function handleState<T>(state: ApiState<T>) {
switch (state.type) {
case "idle":
return "Esperando..."
case "loading":
return "Cargando..."
case "success":
return `Datos: ${state.data} (${new Date(state.timestamp).toISOString()})`
case "error":
if (state.retry) return `Reintentando: ${state.error.message}`
return `Error [${state.code}]: ${state.error.message}`
}
}El switch exhaustivo se asegura con never:
function assertNever(x: never): never {
throw new Error(`Estado no manejado: ${x}`)
}
function handleState<T>(state: ApiState<T>, ctx: Context) {
switch (state.type) { /* ... */ }
default: assertNever(state) // error si falta un case
}El operador satisfies
Introducido en TS 4.9, valida que un tipo cumpla una forma SIN cambiar su tipo inferido:
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Record<string, string | number[]>
palette.red.map(x => x) // ✅ infiere number[], no string | number[]Sin satisfies, palette.red sería string | number[] y no podrías usar .map() sin type guard.
Genéricos avanzados
infer en condicionales
Extrae un tipo desde una estructura mayor:
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never
type Fn = (x: string) => number
type R = ReturnTypeOf<Fn> // numberUtility types reales
Un deepPartial que recorre recursivamente:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? DeepPartial<U>[]
: T[P] extends object
? DeepPartial<T[P]>
: T[P]
}El tsconfig que necesitas
Estas flags atrapan bugs reales que any y null dejan pasar:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"isolatedDeclarations": true
}
}noUncheckedIndexedAccess: cada acceso aarr[i]retornaT | undefined— te obliga a manejar elundefined.exactOptionalPropertyTypes:prop?: stringpermiteundefinedpero no te deja asignarundefineda una propiedad requerida.isolatedDeclarations: necesario para compatibilidad con bundlers que transpilan archivo por archivo.
Branded types — tipado nominal en sistema estructural
type UserId = string & { readonly __brand: "UserId" }
type OrderId = string & { readonly __brand: "OrderId" }
function getUser(id: UserId): User { /* ... */ }
function getOrder(id: OrderId): Order { /* ... */ }
const uid = "abc123" as UserId
getUser(uid) // ✅
getOrder(uid) // ❌ tipo incorrectoSin esto, string es string y las IDs se mezclan. Una fuente frecuente de bugs en producción.
Errores comunes
anyleakage: una función que recibeanyinfecta todo el grafo de tipos. Siempre usaunknownsi el tipo es incierto.asassertion:asdesactiva el type checker. Es una bomba de tiempo. Prefieresatisfieso type guards.- Enums: prefiero uniones de strings literales. Los enums numéricos de TypeScript generan código IIFE y rompen la tree-shaking.
- Seguridad: TypeScript + template strings no sanitiza SQL.
"SELECT * FROM users WHERE id = '${input}'"sigue siendo SQL injection aunque el resto del proyecto esté tipado.
TypeScript bien usado no es overhead — es un multiplicador de fuerza. Cuando escalas de "tipos como anotaciones" a "tipos como diseño", el código se escribe solo.