Elegir un lenguaje según la naturaleza del problema: lecciones de la adaptación de TypeScript al estilo Go
Lo que la elección de Go como compilador nativo para TypeScript enseña sobre cómo adaptar las herramientas a los trabajos a realizar, la importancia de la velocidad en las herramientas y el proceso de portar grandes bases de código paso a paso.
Espere casi un minuto a que se active la función de autocompletado en un gran monorepo de TypeScript: es un problema muy común, y es el mismo que experimentan los desarrolladores móviles mientras Xcode indexa un proyecto grande. El equipo de TypeScript luchó precisamente contra este problema, y la forma en que eligió solucionarlo constituye un estudio de caso útil para tomar decisiones tecnológicas basadas en evidencias y no en modas. Este artículo va más allá de los titulares relacionados con pruebas de rendimiento y destaca el razonamiento técnico que puede aplicar a sus propias herramientas y aplicaciones, ya sea que utilice TypeScript, Swift o ambos.
Lo que anunció Microsoft
En marzo de 2025, Anders Hejlsberg, creador tanto de TypeScript como de C#, anunció que Microsoft estaba desarrollando una versión nativa del compilador de TypeScript y sus herramientas lingüísticas. Muchos supusieron que el objetivo sería Rust o quizás C++. En realidad fue Go.
La implementación nativa, con nombre en código Corsa, es la base de TypeScript 7.0. En el momento de redactar este texto, las previsiones apuntaban a un lanzamiento alrededor de mediados de 2026, por lo que consulte el blog oficial de TypeScript para conocer el estado actual antes de planificar una migración.
Las cifras reportadas fueron significativas: la carga del códigobase de VS Code en el editor disminuyó de aproximadamente 9.6 segundos a 1.2 segundos, el uso de memoria se redujo a la mitad aproximadamente, y Microsoft describió que la carga del proyecto era en general unas 8 veces más rápida, con una verificación de tipos hasta 10 veces más veloz. Si desea conocer los detalles sobre cómo se logran estas mejoras en proyectos reales, los hemos abordado por separado en cómo los cambios en Go mejoran la velocidad sin tocar su código. El resto de este artículo se centra en la decisión en sí.
¿Por qué Go en lugar de Rust?
El equipo buscaba el lenguaje de nivel más bajo que aún pudiera generar código nativo en todas las plataformas que TypeScript debe soportar, al tiempo que ofrecía una buena concurrencia integrada. Go cumple con esa descripción.
Había otros dos factores importantes. Primero, la carga de trabajo: verificar los tipos en un proyecto grande consiste principalmente en revisar muchos archivos en paralelo y luego combinar los resultados, y las goroutines se adaptan directamente a este patrón. Segundo, el código existente: el compilador es una enorme base de código en JavaScript que depende de la recolección de basura y de un estilo bastante funcional. Go también cuenta con un recolector de basura y una estructura similar, por lo que el código podía ser traducido con precisión. Trasladarlo a Rust habría obligado a un rediseño mucho más complejo relacionado con la propiedad y los tiempos de vida, lo cual supondría un costo elevado para un equipo grande que tendría que transferir una base de código enorme.
En pocas palabras, el equipo no eligió el lenguaje más moderno. Escogió aquel que se adaptaba a su modelo de concurrencia y permitía mantener productivos a sus ingenieros. Eso es un juicio arquitectónico, del mismo tipo de decisión que se toma al elegir entre UIKit y SwiftUI, o entre Combine y el enfoque async/await tradicional para una función específica.
La misma idea en Swift
Los desarrolladores de Swift ya han enfrentado este tipo de problema con anterioridad. La concurrencia estructurada se añadió a Swift precisamente para ejecutar tareas costosas en paralelo sin sacrificar la corrección. El fragmento a continuación expresa la idea de “revisar varios archivos al mismo tiempo y luego fusionarlos” mediante un grupo de tareas: cada archivo recibe su propia tarea hija, y el padre recopila los resultados a medida que las tareas finalizan. Cabe señalar que se trata de código en Swift, aunque ilustra el patrón que utiliza el compilador de Go con las goroutines.
// Swift concurrency: parallel "type-checking" of files, similar in spirit
// to how Go's goroutines let TypeScript check many files at once
func checkFiles(_ paths: [String]) async -> [Diagnostic] {
await withTaskGroup(of: [Diagnostic].self) { group in
for path in paths {
group.addTask {
await checkSingleFile(path) // runs concurrently
}
}
var results: [Diagnostic] = []
for await diagnostics in group {
results.append(contentsOf: diagnostics)
}
return results
}
}
Hay varios aspectos que merecen atención. withTaskGroup garantiza que cada tarea hija se complete antes de que la función devuelva un valor, por lo que no hay trabajo que quede fuera del ámbito de ejecución. Los resultados llegan en orden de finalización y no en el orden en que se ingresaron, así que si el orden de los diagnósticos es importante, será necesario ordenarlos posteriormente. Además, el paralelismo solo es seguro porque cada verificación es independiente, lo cual es precisamente la característica que hace que las comprobaciones de tipo sean adecuadas para el modelo de Go.
Si has adoptado Swift 6, ya has hecho este tipo de compromiso: pagas por adelantado para cumplir con estrictas verificaciones de concurrencia y seguridad, y los beneficios se obtienen más tarde en forma de compilaciones más rápidas y un comportamiento en tiempo de ejecución más estable. Microsoft realizó una apuesta similar con su compilador.
Lecciones aplicables a tus propios proyectos
- La velocidad de los herramientas es una característica del producto. Las compilaciones lentas y el autocompletado deficiente imponen un costo diario, en gran medida invisible, a cada desarrollador del equipo. El trabajo de mejora de rendimiento en las pipelines de compilación, los editores y el indexado es tan legítimo como el trabajo de mejora de rendimiento en la aplicación final.
- Deje que el problema, no la tendencia, elija la herramienta. Ya sea que opte por MVVM, SwiftData o Combine, la respuesta correcta surge de cómo fluyen realmente los datos en su sistema, y no de lo que sea popular en las redes sociales este mes.
- Las reescrituras pueden ser incrementales. El equipo de TypeScript portó el compilador existente con la mayor fidelidad posible en lugar de rediseñarlo, por lo que la nueva versión produce los mismos resultados que la antigua. Eso es un argumento sólido en contra de reconstruir toda la arquitectura de una aplicación en un único sprint.
¿Cuándo no debería seguir este ejemplo? Si su pila actual no es el cuello de botella, adoptar una solución alternativa solo le traerá riesgos. Mida primero y asegúrese de que la parte lenta realmente se encuentre en la capa que pretende reemplazar.
Reconociendo las señales de advertencia
Los síntomas que llevaron a este proyecto tienden a aparecer en cualquier base de código que crezca en tamaño: la inferencia de tipos que se ralentiza, las respuestas del editor que se retrasan y los gráficos de compilación que siguen expandiéndose. La solución no siempre es un nuevo framework; a veces significa examinar detenidamente lo que realmente hacen sus herramientas en el fondo y modificar la parte que realiza demasiado trabajo.
Puntos clave
- Se eligió Go porque su compilación nativa, su recolección de basura y su concurrencia basada en goroutines se adaptaban tanto a la carga de trabajo de verificación de tipos como a la estructura del código existente.
Lecturas relacionadas
- La verdadera complejidad de JavaScript moderno proviene de las herramientas, no del idioma — Este artículo explica cómo las características fundamentales de JavaScript como async/await y la cadena opcional simplifican el código, mientras que las herramientas y dependencias excesivas generan una complejidad innecesaria.
- Dentro de la reescritura en Go de TypeScript 7: Mejoras de velocidad sin cambios en el código — Aprenda cómo el compilador basado en Go de TypeScript 7 permite compilaciones 8-12 veces más rápidas, por qué funciona este cambio de arquitectura y cómo actualizar de forma segura proyectos existentes.
- De un diseño a muchas pantallas: Un flujo de trabajo más sensato para los elementos iniciales — Por qué los elementos iniciales consumen más tiempo del que su diseño merece, cómo crear uno que funcione con cualquier relación de aspecto y qué debería hacer un generador basado en el navegador enfocado en esta tarea.
- Decisiones de actualización a TypeScript 7: Verificadores paralelos, modo observación y fallos en la API — Qué cambia al pasar de TypeScript 6 a TypeScript 7, nativo de Go; cómo funcionan las nuevas banderas de paralelismo y qué herramientas deberían esperar antes de actualizarse.
- De escribir Kotlin a dirigir agentes: el nuevo rol del ingeniero móvil — Cómo la programación de agentes transforma el trabajo del ingeniero de Android hacia las especificaciones, el contexto, las restricciones arquitectónicas y la verificación, y qué principios fundamentales son más importantes que nunca.