¿Cuánto tarda en romperse un proyecto sin tipos? Con una persona que lo escribió todo, casi nunca: esa persona sabe qué devuelve cada función. Con tres personas y dos años de cambios, bastante antes de lo que nadie espera.
La discusión sobre tipado suele plantearse como una cuestión de gustos, y eso oculta lo que de verdad está en juego. No se trata de si los tipos hacen el código más correcto, sino de quién guarda la información sobre la forma de los datos: la cabeza de alguien o el propio código.
Qué detecta el tipado, y qué no
Conviene empezar por el límite, porque es donde más expectativas equivocadas hay.
Un sistema de tipos detecta errores de forma: pasar un texto donde se esperaba un número, acceder a un campo que no existe, olvidar que un valor puede ser nulo, llamar a una función con los argumentos en el orden equivocado.
No detecta errores de lógica: calcular mal un descuento, aplicar un impuesto dos veces, olvidar comprobar un permiso. Un programa perfectamente tipado puede estar completamente equivocado.
Esa distinción desactiva los dos argumentos extremos. Quien dice que los tipos eliminan los errores exagera; quien dice que las pruebas los hacen innecesarios ignora que escribir pruebas para comprobar la forma de cada dato es trabajo que un compilador hace gratis.
Cuándo aparece el error
La diferencia práctica entre los dos enfoques no es si el error ocurre, sino en qué momento se descubre.
Con tipado estático, el editor marca en rojo la llamada incorrecta mientras escribes. Con tipado dinámico, el mismo error aparece al ejecutar esa línea, lo que puede ser en una prueba, o puede ser un martes de madrugada cuando un usuario llega a ese camino poco transitado.
La palabra importante del diagrama es si hay pruebas. En un proyecto dinámico con buena cobertura, casi todo se detecta antes de producción. En uno sin pruebas, que es la situación más común en proyectos pequeños, el primer aviso lo da un usuario.
El beneficio que no es detectar errores
Hay una ventaja de los tipos que suele pesar más que la detección y que casi nunca aparece en la discusión: la documentación que no se desactualiza.
Un comentario que dice que una función devuelve un objeto con tres campos puede quedar obsoleto el día que alguien añade un cuarto. Una declaración de tipo no puede, porque si deja de coincidir con el código, el programa no compila.
Eso cambia la experiencia de trabajar con código ajeno. En lugar de leer la implementación para saber qué forma tienen los datos, el editor lo muestra al pasar el cursor. Y al cambiar algo, señala todos los sitios afectados antes de ejecutar nada.
Ese último punto es el que más se nota al cabo del tiempo: renombrar un campo en un proyecto grande sin tipos es buscar texto y rezar. Con tipos, es una operación del editor que no deja ningún uso sin actualizar.
Lo que cuesta de verdad
El precio existe y conviene no minimizarlo, porque es la razón de que esta decisión siga abierta.
Escribir tipos lleva tiempo, sobre todo al principio, cuando la forma de los datos todavía cambia cada día. En un prototipo que quizá se tire la semana que viene, ese esfuerzo se desperdicia.
Los sistemas de tipos avanzados tienen además su propia complejidad. Es posible pasar una tarde entera intentando expresar un tipo que el lenguaje no sabe representar bien, y ese tiempo no produce ninguna funcionalidad.
Y hay una trampa en la dirección contraria: un tipado laxo da falsa seguridad. Un proyecto lleno de tipos genéricos que aceptan cualquier cosa tiene todo el coste de escribir tipos y casi ninguna de sus ventajas.
El camino intermedio que se impuso
La discusión perdió buena parte de su intensidad porque la industria encontró una salida: lenguajes dinámicos a los que se les añade tipado de forma gradual.
TypeScript sobre JavaScript, las anotaciones de tipo en Python y los tipos declarados en PHP moderno permiten empezar sin tipos y añadirlos donde más rinden, sin reescribir nada. El código sigue ejecutándose igual; lo que cambia es que una herramienta comprueba lo anotado.
# Python: comprobar las anotaciones sin cambiar cómo se ejecuta el código
pip install mypy
mypy app/ --strict
# PHP: análisis estático de tipos, empezando por un nivel bajo
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src --level=5
# TypeScript: comprobar sin generar archivos
npx tsc --noEmit
La ventaja de este enfoque es que la decisión deja de ser todo o nada. Se puede empezar por los tipos de los datos que cruzan fronteras, como las respuestas de la API y los modelos de la base, que es donde un error de forma hace más daño, y dejar para después el resto.
Dónde rinden más los tipos
Si hay que priorizar, estos son los puntos donde un tipo evita más problemas por línea escrita.
- Las fronteras del sistema. Lo que entra de una API externa, de un formulario o de la base de datos. Ahí la forma no la controlas tú, y un tipo junto con una validación en tiempo de ejecución convierte datos desconocidos en datos fiables.
- Los valores que pueden faltar. Obligar a manejar el caso nulo elimina la clase de error más común de todos los lenguajes.
- Las funciones compartidas. Lo que usa medio proyecto es lo que más duele cambiar, y lo que más se beneficia de que el editor muestre todos sus usos.
- Los estados cerrados. Un pedido que solo puede estar pendiente, pagado o enviado, representado como un conjunto cerrado de valores y no como un texto libre.
Ese último punto evita un fallo muy concreto: un texto con una errata, como "pagdo", que pasa todas las comprobaciones y deja un pedido en un estado que ninguna parte del código reconoce.
Los tipos no validan lo que llega de fuera
Hay una confusión que produce fallos en producción y conviene aclararla, porque afecta sobre todo a TypeScript.
Los tipos se comprueban al compilar y desaparecen al ejecutar. Si declaras que la respuesta de una API tiene cierta forma, el compilador te cree, pero nada comprueba que la respuesta real la tenga. Si el servicio externo cambia, el código recibe datos distintos de lo declarado y falla más adelante, en un sitio que no parece relacionado.
Para los datos que vienen de fuera hacen falta dos cosas a la vez: el tipo, para el editor, y una validación en tiempo de ejecución, para la realidad. Varias librerías permiten definir la forma una sola vez y obtener ambas cosas.
La regla práctica es no escribir nunca una aserción de tipo sobre datos externos sin validarlos. Es la forma de mentirle al compilador, y el compilador no tiene manera de saberlo.
Añadir tipos a un proyecto que ya existe
La situación más habitual no es elegir desde cero, sino tener un proyecto funcionando sin tipos y querer mejorarlo sin detenerlo todo. Hay un orden que funciona y otro que acaba abandonado a la mitad.
El que se abandona es activar el modo estricto en todo el proyecto de una vez. Aparecen cientos de errores, nadie tiene una semana para corregirlos, y la configuración vuelve al modo permisivo al día siguiente.
El que funciona es el inverso: activar la comprobación en un nivel bajo para que el proyecto pase tal como está, y exigir el nivel estricto solo en los archivos nuevos o en los que se toquen. Así el tipado avanza con el trabajo normal, sin una tarea aparte que nunca llega a priorizarse.
# PHPStan: registrar los errores actuales como punto de partida aceptado
vendor/bin/phpstan analyse src --level=6 --generate-baseline
# A partir de aquí, solo fallan los errores NUEVOS que se introduzcan
vendor/bin/phpstan analyse src --level=6
# TypeScript: permitir JavaScript existente mientras se migra archivo a archivo
npx tsc --noEmit --allowJs --checkJs false
La línea base es la pieza que hace viable la migración: congela la deuda existente para que no bloquee a nadie, e impide que crezca. Cada vez que alguien corrige un archivo, la línea base se regenera más pequeña.
Conviene además incorporar esa comprobación al proceso de integración, de modo que un cambio que introduzca un error de tipo nuevo no se pueda fusionar. Sin ese candado, la mejora depende de que todo el mundo se acuerde de ejecutarla.
Cómo decidir según el proyecto
La decisión depende menos del lenguaje que de tres características del proyecto.
La vida esperada: un script de un solo uso no necesita tipos; un sistema que se mantendrá años, casi seguro que sí. El número de personas: con una sola, la información vive en su cabeza; con varias, tiene que vivir en el código. Y el coste de un fallo: un error de forma en un panel interno es una molestia, en el cálculo de una factura es un problema.
Para un proyecto que empieza y aspira a durar, la recomendación razonable es empezar con tipado gradual activado en modo estricto desde el primer día. Añadir tipos a un proyecto grande sin ellos es mucho más costoso que mantenerlos mientras crece.
Qué se le escapa a una IA con esto
El código generado con TypeScript tiene un vicio muy reconocible: cuando un tipo se complica, aparece un tipo genérico que acepta cualquier cosa o una aserción que obliga al compilador a callarse. El código compila y la comprobación de tipos deja de proteger justo donde más hacía falta.
Hay también un patrón con datos externos: se declara el tipo de la respuesta de una API y se usa directamente, sin validar nada en tiempo de ejecución, lo que funciona hasta que el servicio cambia su formato.
Las frases que cambian la respuesta son pedir que no se use ningún tipo genérico abierto ni aserciones para silenciar al compilador, y que todo dato externo se valide en tiempo de ejecución antes de tratarlo como tipado.
# Contar cuántas veces se esquiva el sistema de tipos en un proyecto TypeScript
grep -rnE ':\s*any\b|as any|@ts-ignore|@ts-expect-error' src/ | wc -l
Ese número, revisado de vez en cuando, dice más sobre la salud del tipado de un proyecto que cualquier configuración.