Por qué amo TypeScript: la asincronía como examen de honestidad
Amo TypeScript por una razón poco romántica: me obliga a admitir lo que mi código realmente devuelve. Y en ningún lugar eso duele más que en la asincronía.
Promise<T> es el tipo más honesto del lenguaje
Promise<User> no dice “un usuario”. Dice “un usuario, eventualmente, si nada explota”. Esa palabra —eventualmente— es la que la mayoría del código ignora hasta que un test flaky la recuerda.
// Lo que la gente cree que escribió
const user = getUser(id);
// Lo que escribió
const user: Promise<User> = getUser(id);
TypeScript no te salva de la concurrencia, pero te impide mentirte sobre ella. Sin tipos, user.name es undefined en runtime. Con tipos, es un error rojo antes del commit. La diferencia entre ambas cosas son unas seis horas de tu vida un jueves.
async/await no es paralelismo, es azúcar secuencial
El error más caro que veo en revisiones:
const perfil = await getPerfil(id); // 300ms
const permisos = await getPermisos(id); // 300ms
const feature = await getFlags(id); // 300ms
// 900ms, y ninguna depende de la otra
Versus:
const [perfil, permisos, flags] = await Promise.all([
getPerfil(id),
getPermisos(id),
getFlags(id),
]);
// 300ms, y el tipo de la tupla lo infiere solo
Promise.all tipa la tupla posición por posición. Cambias el orden y TypeScript se queja. Es el tipo de detalle aburrido que evita un bug de producción que ninguna retro habría explicado bien.
Y cuando una de las tres puede fallar sin arrastrar al resto, existe Promise.allSettled. Devuelve { status: 'fulfilled' | 'rejected' } y te fuerza a decidir qué haces con el fracaso parcial en vez de dejar que un catch gigante se lo trague todo.
El agujero que TypeScript no tapa: catch (e: unknown)
Aquí es donde el lenguaje es honesto hasta la incomodidad. Cualquiera puede hacer throw "algo", así que el tipo del error es unknown y no hay nada que TypeScript pueda hacer al respecto.
try {
await pagar(orden);
} catch (e: unknown) {
if (e instanceof PagoRechazado) return reintentar(orden);
throw e; // no sé qué es esto, no lo voy a inventar
}
Esa última línea es la lección: propagar lo que no entiendes es mejor que loguearlo y seguir como si nada. El catch que se traga todo es el equivalente en código a asentir en una reunión donde te perdiste hace diez minutos.
Cancelación: lo que casi nadie tipa
AbortController existe desde hace años y sigue siendo el patrón menos usado del ecosistema. Un usuario que escribe rápido en un buscador dispara ocho requests; sin cancelación, la respuesta que pinta la pantalla es la que llegó última, no la que corresponde a lo que está escrito. Eso es una race condition, no un “comportamiento raro del navegador”.
function buscar(q: string, signal: AbortSignal): Promise<Resultado[]> {
return fetch(`/api?q=${q}`, { signal }).then((r) => r.json());
}
Poner signal en la firma convierte una convención opcional en un requisito del tipo. Quien llame a esa función tiene que pensar en cancelar. Eso es diseño, no burocracia.
Por qué esto importa más que los genéricos elegantes
TypeScript se vende con tipos condicionales y magia a nivel de tipos, y esa parte es entretenida para conferencias. El valor real es más pedestre: te obliga a nombrar el tiempo. Qué está listo ahora, qué estará listo después, qué puede fallar en el medio.
El código asíncrono sin tipos no es más rápido de escribir. Solo posterga la conversación con la realidad hasta que la realidad tenga usuarios.