Qué captura el compilador de TypeScript y qué pasa desapercibido en JavaScript
Compare JavaScript y TypeScript lado a lado: inferencia de tipos, anotaciones, primitivos, any, uniones y funciones tipadas, además de lo que realmente marca y emite tsc.
Fuentes: JavaScript vs TypeScript: Understanding Type Safety
JavaScript puede ejecutar casi cualquier cosa que se le proporcione, y esa es precisamente la razón por la cual algunos errores solo aparecen en entornos de producción. TypeScript agrega un paso de verificación antes de que se ejecute el código, pero es fácil malinterpretar qué protege y qué no. Esta guía utiliza ejemplos sencillos que permiten trabajar con tipos, desde la reasignación de variables hasta funciones tipadas, para que pueda ver exactamente dónde JavaScript permanece silencioso, dónde el compilador de TypeScript interviene y cómo configurarlo para que los errores impidan realmente la compilación.
Tipo dinámico versus tipo estático
JavaScript es de tipo dinámico. Una variable no tiene un tipo fijo; su tipo corresponde al valor que actualmente contiene, se determina mientras el programa se ejecuta y puede cambiar en cualquier momento.
TypeScript es un superset de JavaScript con tipado estático. Cada programa válido en JavaScript también es válido sintácticamente en TypeScript, pero TypeScript añade un verificador de tipos que analiza tu código en tiempo de compilación, antes de que se ejecute cualquier cosa. A continuación, el verificador elimina la información de tipo y genera JavaScript puro.
Todo lo demás en esta guía se deriva de esa única diferencia. Decidir los tipos en tiempo de ejecución hace que JavaScript sea flexible, pero permite que los errores pasen desapercibidos. Definir los tipos por adelantado hace que TypeScript sea más estricto, pero detecta muchos errores temprano, y eso es a lo que se refieren cuando dicen que TypeScript es seguro en cuanto a tipos. Estrictamente hablando, esa seguridad es una garantía en tiempo de compilación: solo es válida dentro del alcance que puede ver el verificador.
Cuando una variable cambia de tipo
En JavaScript puro, comienza con una variable numérica y verifica su tipo con typeof:
let age = 30;
console.log(typeof(age)); // output is number
Unas líneas más adelante, nada impide almacenar una cadena en la misma variable:
age = "John";
console.log(age);
console.log(typeof(age)); // string
JavaScript lo acepta sin advertencias ni errores. El primer typeof muestra "number" y el segundo, "string": una variable con dos tipos no relacionados a lo largo de su vida útil. En un script pequeño esto no supone problema; pero en una base de código grande, cualquier código que siga esperando un número del parámetro age comportarseá incorrectamente, lejos de la línea que causó el problema.
TypeScript rechaza el código equivalente:
let data = 2;
data = "two"; // ❌ Error: Type 'string' is not assignable to type 'number'
Cuando TypeScript ve let data = 2;, infiere a partir del valor inicial que data tiene el tipo number, y mantiene ese tipo durante toda la vida útil de la variable. Este comportamiento se denomina inferencia de tipos. Por lo tanto, la asignación de "two" en la línea siguiente es rechazada por el compilador antes incluso de que el programa se ejecute.
En lugar de depender de la inferencia, puedes especificar el tipo tú mismo:
let data: number = 2;
La parte : number es una sintaxis exclusiva de TypeScript y no existe en JavaScript. Para la seguridad de tipos, las dos formas son equivalentes; la versión anotada simplemente hace que la intención sea visible para cualquiera que lea el código más adelante, incluido tú en seis meses.
La regla a recordar: JavaScript permite que una variable cambie de tipo tantas veces como se desee, mientras que TypeScript fija el tipo de una variable una sola vez, ya sea a través de sus anotaciones o de su primer valor, y lo aplica desde entonces.
La trampa de cadena más número
El ejemplo anterior muestra que TypeScript verifica los tipos. El siguiente explica por qué eso es importante.
JavaScript, una vez más, permite sin problemas que una variable que contenía una cadena sea sobrescrita con un número:
let message = "Hello";
let num = 2;
message = 20; // totally allowed in JS
Ahora la sorpresa clásica:
let result = "5" + 3;
console.log(result); // "53", not 8
Podría esperarse 8, ya que "5" parece numérico. Pero "5" es una cadena, y cuando el operador + encuentra cadenas en ambos lados, JavaScript realiza una concatenación de cadenas en lugar de operaciones aritméticas. Convierte 3 en texto y une los dos, produciendo "53". JavaScript ejecuta esto sin problemas porque nunca verifica los tipos por adelantado; simplemente hace lo que el código indica literalmente en el momento en que se ejecuta.
Qué hace TypeScript con el mismo código
Aquí está la versión en TypeScript, con los tipos especificados:
let num1: string = "5";
let num2: number = 3;
Y la suma en sí:
let add = num1 + num2; // expected 8, but num1 is a string and num2 is a number
console.log(add); // actual result is 53
El valor impreso sigue siendo 53. Aquí es donde muchas explicaciones fallan, por lo que vale la pena ser preciso. TypeScript permite deliberadamente string + number, ya que concatenar texto con un número es una práctica común y legítima en JavaScript. El verificador no rechaza la expresión; infiere que add tiene el tipo string.
El error de operador que podrías haber visto se ve así:
Operator '+' cannot be applied to types 'string' and 'number'.
TypeScript muestra este tipo de mensaje para combinaciones de operandos que considera sin sentido, como sumar un boolean con un number, o sumar valores cuyos tipos no tienen nada en común. No es lo que ocurre con num1 + num2 anteriormente.
En este escenario, TypeScript te protege en el momento en que el resultado se utiliza como número. Si hubieras escrito let add: number = num1 + num2;, el compilador indicaría que el tipo string no puede asignarse al tipo number. De igual manera, pasar add a una función que espera un number causaría un error. En la práctica, así es como se detecta el error de “5” + 3 en un código tipado: no en el signo más, sino dondequiera que una cadena de texto accidental se encuentre con código para el cual se esperaba un número.
Por qué los errores no detienen tu programa por defecto
Existe una segunda sutileza. Incluso cuando tsc encuentra un error de tipo, por defecto sigue generando el archivo .js compilado. Al ejecutar ese archivo con node, se ejecuta simplemente JavaScript, nada más. Toda la información de tipo ha sido eliminada y solo queda la lógica en tiempo de ejecución.
Esta es la esencia de TypeScript: no modifica el comportamiento de tu código en tiempo de ejecución. Detecta problemas en tu editor y durante la compilación, antes de que ejecutes cualquier cosa. Si deseas que el compilador se niegue a generar salida cada vez que haya errores de tipo, activa esta opción en tsconfig.json:
{
"compilerOptions": {
"noEmitOnError": true
}
}
Al tener activado noEmitOnError, el código con errores de tipo nunca llega a node, ya que tsc se detiene sin escribir el archivo JavaScript. En la mayoría de los proyectos reales, se logra el mismo efecto en CI ejecutando tsc --noEmit como paso separado de verificación de tipos, lo que hace que la compilación falle ante cualquier error.
El punto clave: el valor de TypeScript no radica en un comportamiento diferente en tiempo de ejecución, ya que el TypeScript compilado es JavaScript. Su valor está en detectar incoherencias antes de que pulses ejecutar, dándote la oportunidad de corregir errores que JavaScript enviaría silenciosamente.
Anotaciones e inferencia
Estos dos términos describen formas opuestas de llegar al mismo resultado. Considere una declaración anotada:
let age: number = 30;
Analicemos cada parte:
letes la palabra clave de declaraciónagees el nombre de la variable30es el valor asignadonumberes el tipo: number, el dos puntos seguido del tipo, es la anotación de tipo
Una anotación indica explícitamente a TypeScript qué tipo tiene una variable, utilizando una sintaxis de dos puntos como : number, : string o : boolean. La inferencia es lo contrario: se omite el tipo y TypeScript lo deduce a partir del valor asignado.
let age = 30; // inference — TS decides this is a "number" on its own
let city: string = "Delhi"; // annotation — you told TS explicitly
Ambos brindan la misma protección, ya que una vez que se establece el tipo, sea por usted o por el compilador, queda fijo. Las anotaciones sirven principalmente para mayor claridad. Son más útiles cuando una variable se declara sin un valor inicial, ya que no hay nada de lo cual inferir, así como en las firmas de funciones y las APIs públicas, donde documentan la intención. Para variables locales con un valor inicial obvio, la mayoría de los equipos dejan que la inferencia haga el trabajo.
Tipos primitivos y no primitivos
Una forma útil de agrupar los tipos que conocerá al principio es:
- Tipos primitivos como
number,string,boolean,undefinedynullalbergan exactamente un valor a la vez.
Un primitivo es una caja que contiene un único elemento; un tipo estructurado es una caja grande que alberga muchas cajas.
Hay dos constructos más que suelen aparecer en las listas para principiantes junto a los primitivos, y se tratan a continuación: any y los tipos union. Estrictamente hablando, ninguno de ellos es primitivo. any es una solución de emergencia que desactiva las verificaciones, mientras que un tipo union es una forma de combinar otros tipos. Se encuentran junto a los primitivos porque típicamente se usan con valores individuales.
Tipos primitivos en la práctica
Números
El tipo number de TypeScript abarca tanto números enteros como valores en punto flotante. No existen tipos separados de int o float como en muchos otros lenguajes, ya que JavaScript almacena los números comunes como valores en punto flotante de 64 bits.
let age = 20;
let price = 26.2;
let big = 24757689;
Al imprimirlos y verificar su tipo con typeof:
console.log("Age :", age);
console.log("price:", price);
console.log(big);
console.log(typeof(age)); // "number"
Tanto los números enteros como las decimales y los valores grandes son simplemente de tipo number, y typeof devuelve "number" para cada uno de ellos. Para los enteros que exceden el rango seguro de ese formato en punto flotante, JavaScript ofrece un tipo separado llamado bigint.
Cadenas de texto
Una cadena de texto es una secuencia de caracteres: una palabra, una oración o cualquier texto. Puede escribir literales de cadena con comillas simples, comillas dobles o backticks.
let firstname: string = 'JOHN';
let lastname: string = "SINGH";
console.log("Hello", firstname, lastname); // Hello JOHN SINGH
Al pasar varios argumentos separados por comas a console.log, estos se imprimen separados por espacios, y por eso aquí se muestra el nombre completo. Sin embargo, una línea relacionada no hace lo que parece hacer:
// let greeting: string = "Hello", firstname, lastname; // ❌ this does NOT concatenate
Dentro de una declaración let, la coma sirve para separar múltiples declaraciones; no se utiliza para unir texto. Esta línea declara tres variables separadas: greeting, firstname y lastname, y solo greeting recibe un valor. Si firstname y lastname ya existen en el mismo ámbito, TypeScript también indicará que se están redeclarando.
Así que las comas sirven para imprimir varios valores, pero para almacenar una oración combinada en una variable, se debe utilizar un literal de plantilla: una cadena entre comillas invertidas, con marcadores ${} para las expresiones.
let firstname: string = 'JOHN';
let lastname: string = "SINGH";
let greeting: string = `Hello ${firstname} ${lastname}`;
console.log(greeting); // Hello JOHN SINGH
Los literales de plantilla son la forma recomendada para combinar texto y valores. Se leen de manera más clara que las cadenas de concatenación con + y también permiten strings que abarcan varias líneas.
Valores booleanos
Un valor booleano tiene exactamente dos valores posibles: true y false.
let isStudent: boolean = true;
let hasJob: boolean = false;
Registro de uno y verificación de su tipo:
console.log("Is student?", isStudent); // true
console.log(typeof (isStudent)); // "boolean"
Los valores booleanos controlan todas las condiciones en su código: instrucciones if, condiciones de bucle y cualquier otro lugar donde se tome una decisión de sí o no.
Null y undefined
Tanto null como undefined representan la ausencia de un valor, pero tienen diferentes significados. Comience con una variable que se declara pero nunca se asigna:
let emptyvalue;
console.log(typeof emptyvalue); // "undefined"
Una variable declarada pero no asignada contiene undefined, lo que significa que la variable existe, pero aún no se ha almacenado nada en ella.
También se pueden asignar explícitamente a las variables los tipos null y undefined:
let emptyvalue: null = null;
let value: undefined = undefined
Luego compare cómo se muestra cada uno, junto con una variable de tipo que no ha sido asignada:
console.log(emptyvalue); // null
console.log(value); // undefined
let price: number;
console.log(price); // undefined - declared, but not yet assigned any value
La diferencia en términos sencillos:
undefinedsignifica que una variable existe pero no se le ha asignado un valor; es el estado vacío por defecto.nullsignifica que un desarrollador estableció deliberadamente la variable en “ningún valor”; es un vacío intencional y explícito.
Una anotación de tipo como : number restringe qué puede contener una variable en el futuro; no proporciona un valor inicial. Por lo tanto, en tiempo de ejecución, price muestra undefined. No obstante, cabe señalar que al tener activado el modo strict, lo cual es la norma en los proyectos modernos, TypeScript reporta un error en esa línea de console.log(price) porque la variable se utiliza antes de ser asignada. Esa verificación es otro ejemplo más de cómo el compilador detecta un error que JavaScript puro permitiría.
En el código cotidiano rara vez anotará una variable como puramente null o undefined; estos ejemplos existen para mostrar cómo se comportan los dos estados vacíos. Mucho más común es una unión como string | null para un valor que podría faltar.
El tipo any: desactivar la verificación
Use any con poca frecuencia y de forma deliberada, ya que desactiva precisamente las verificaciones para las cuales se utilizó TypeScript. Una variable any acepta todo tipo de valor y puede reasignarse a un tipo diferente según se desee, al igual que en JavaScript puro.
let value: any = "abcd";
console.log(typeof value); // "string"
Reasignándola a un número y luego a un booleano:
value = 123;
console.log(typeof value); // "number"
value = true;
console.log(typeof value); // "boolean"
La variable pasa de ser una cadena de texto a un número y luego a un booleano sin ninguna objeción, porque la anotación indicó al compilador que no la verificara.
Existen usos legítimos para ello. Cuando realmente no se puede conocer un tipo con antelación, por ejemplo, con datos externos impredecibles, o durante una migración incremental desde JavaScript cuando no es práctico escribir todo de golpe, any permite seguir avanzando. Sin embargo, depender excesivamente de él conlleva la pérdida de la función de autocompletado, errores en tiempo de compilación y la protección que motivó inicialmente el uso de TypeScript. Considérelo como un último recurso, no como un hábito. Para datos de forma desconocida, unknown suele ser la mejor opción, ya que le obliga a verificar el valor antes de usarlo. Nuestra guía sobre cómo reemplazar any por patrones seguros desde el punto de vista tipológico aborda en profundidad estas alternativas.
Tipos de unión: un conjunto controlado de opciones
Un tipo de unión permite que un valor sea uno de varios tipos específicos. A diferencia de any, no admite todo, solo los tipos que se indiquen. “Union” no es una palabra clave; es el nombre del patrón, escrito con el símbolo de tubería |.
let id: number | string | boolean;
Asignando cada tipo permitido por turnos:
id = "123abc";
console.log(id);
id = 12;
console.log(id);
id = true;
console.log(id);
La variable id puede contener un número, una cadena de texto o un booleano, y nada más. La asignación de un array como [1, 2, 3] es rechazada de inmediato, ya que los arrays no forman parte de esta unión. Esto le brinda flexibilidad justo donde se necesita; los identificadores del mundo real, por ejemplo, a veces llegan como números y otras como cadenas de texto, al tiempo que se mantiene un límite estricto sobre lo que está permitido. Antes de utilizar un valor de la unión de manera específica para un tipo, TypeScript espera que lo restrinja, por ejemplo mediante una comprobación con typeof.
Tipado de funciones
Las funciones agrupan una única tarea en código que se puede reutilizar. Se define una vez y se llama o se invoca cada vez que sea necesario.
Una función sin parámetros
La versión más simple codifica sus valores de forma fija:
function sum() {
console.log(10 + 20);
}
Definir la función no la ejecuta. No sucede nada hasta que se llama a ella:
sum(); // calling the function → prints 30
Esa separación es precisamente el objetivo: escribir la lógica una vez y ejecutarla cuando sea necesario.
Agregar parámetros tipados
Las funciones se vuelven realmente reutilizables una vez que aceptan parámetros, marcadores que permiten que el mismo código opere con diferentes valores en cada llamada.
function sum(x: number, y: number) {
console.log(x + y);
}
Llamarla con dos números:
sum(10, 20); // prints 30
Ahora sum ya no está vinculado a 10 y 20; sum(5, 7) o sum(100, 1) funcionan igual de bien. El : number después de cada parámetro es una anotación de tipo para dicho parámetro, por lo que TypeScript rechaza llamadas como sum("hello", 20) antes de que se ejecute el código. Aquí es donde TypeScript suele demostrar su utilidad: en los límites de las funciones, cuando una parte del programa transfiere datos a otra.
Esta versión sigue imprimiendo su resultado únicamente con console.log. No devuelve nada al llamante, lo cual es importante para el siguiente paso.
Declarar un tipo de retorno
Una función que devuelve su resultado es mucho más útil:
function sum(x: number, y: number): number {
return x + y;
}
Capturando y utilizando el valor devuelto:
let res: number = sum(10, 20);
console.log(res); // 30
Tres cosas han cambiado en comparación con la versión anterior:
return x + y;devuelve el valor al llamante en lugar de imprimirlo dentro de la función.- El
: numberdespués de la lista de parámetros, en(x: number, y: number): number, es la anotación de tipo de retorno. Indica que la función siempre devolverá un número, y devolver una cadena generaría un error de compilación. - Dado que
sumahora devuelve un valor,let res: number = sum(10, 20);puede almacenarlo para usarlo posteriormente: registrarlo, pasárselo a otra función o combinarlo con otros resultados.
Una función que devuelve su resultado puede integrarse en otra lógica; una que solo realiza registros es un callejón sin salida cuya salida no puede reutilizarse. TypeScript a menudo puede inferir los tipos de retorno, pero escribirlos explícitamente en las funciones exportadas evita cambiar accidentalmente lo que devuelve una función.
Conclusión
- JavaScript determina los tipos en tiempo de ejecución y confía plenamente en el usuario, lo cual es flexible pero permite errores silenciosos como que
"5" + 3genere"53". - TypeScript establece el tipo de cada variable mediante anotaciones o inferencia y lo verifica en tiempo de compilación, antes de que se ejecute el código.
- TypeScript no rechaza todas las expresiones dudosas; se permite la concatenación de cadenas con números, y la protección surge cuando la cadena resultante se encuentra con código que espera un número.
- Por defecto, los errores de tipo no detienen la generación de salida; utilice
noEmitOnErroro el parámetrotsc --noEmiten los procesos CI para que impidan la compilación. - Mantenga
anypara casos realmente desconocidos, prefiera uniones yunknownpara mayor flexibilidad, y anote los parámetros de función y los tipos de retorno cuando los datos crucen límites.
El resultado en tiempo de ejecución del código JavaScript y TypeScript es idéntico. Lo que cambia es el momento en que se detecta un problema: en su editor, y no frente a sus usuarios.
Lecturas relacionadas
- Diez patrones de TypeScript que convierten errores en tiempo de ejecución en errores de compilación — Aprenda diez técnicas de TypeScript, desde uniones discriminadas y satisfies hasta tipos branded e infer, que permiten al compilador rechazar estados inválidos antes de que su código sea distribuido.
- Comportamientos de TypeScript que sorprenden a desarrolladores experimentados, y por qué — Tipado estructural, verificaciones excesivas de propiedades, los tipos as const, satisfies, condicionales y mapeados, además de los principios de diseño que los convierten en código más seguro.