TypeScript vs. JavaScript: su guía de referencia

Publicado: 2022-03-11

¿Mecanografiado o JavaScript? Los desarrolladores contemplan esta opción para proyectos web nuevos o Node.js, pero también es una pregunta que vale la pena considerar para proyectos existentes. Un superconjunto de JavaScript, TypeScript ofrece todas las funciones de JavaScript más algunas ventajas adicionales. TypeScript nos alienta intrínsecamente a codificar de manera limpia, lo que hace que el código sea más escalable. Sin embargo, los proyectos pueden contener tanto JavaScript simple como queramos, por lo que usar TypeScript no es una propuesta de todo o nada.

La relación entre TypeScript y JavaScript

TypeScript agrega un sistema de tipo explícito a JavaScript, lo que permite la aplicación estricta de tipos de variables. TypeScript ejecuta sus comprobaciones de tipo durante la transpilación , una forma de compilación que convierte el código TypeScript en el código JavaScript que entienden los navegadores web y Node.js.

Ejemplos de TypeScript frente a JavaScript

Comencemos con un fragmento de JavaScript válido:

 let var1 = "Hello"; var1 = 10; console.log(var1);

Aquí, var1 comienza como una string y luego se convierte en un number .

Dado que JavaScript solo se escribe de forma flexible, podemos redefinir var1 como una variable de cualquier tipo, desde una cadena hasta una función, en cualquier momento.

Ejecutar este código da como resultado 10 .

Ahora, cambiemos este código a TypeScript:

 let var1: string = "Hello"; var1 = 10; console.log(var1);

En este caso, declaramos que var1 es una string . Luego tratamos de asignarle un número, lo cual no está permitido por el estricto sistema de tipos de TypeScript. La transpilación da como resultado un error:

 TSError: ⨯ Unable to compile TypeScript: src/snippet1.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'. 2 var1 = 10;

Si tuviéramos que indicar al transpiler que tratara el fragmento de JavaScript original como si fuera TypeScript, el transpiler inferiría automáticamente que var1 debería ser una string | number string | number Este es un tipo de unión de TypeScript, que nos permite asignar a var1 una string o un number en cualquier momento. Habiendo resuelto el conflicto de tipos, nuestro código TypeScript se transpilaría con éxito. Ejecutarlo produciría el mismo resultado que el ejemplo de JavaScript.

TypeScript vs. JavaScript desde 30,000 pies: desafíos de escalabilidad

JavaScript es omnipresente, impulsando proyectos de todos los tamaños, aplicado en formas que habrían sido inimaginables durante su infancia en la década de 1990. Si bien JavaScript ha madurado, se queda corto en lo que respecta al soporte de escalabilidad. En consecuencia, los desarrolladores se enfrentan a aplicaciones de JavaScript que han crecido tanto en magnitud como en complejidad.

Afortunadamente, TypeScript soluciona muchos de los problemas de escalar proyectos de JavaScript. Nos centraremos en los tres principales desafíos: validación, refactorización y documentación.

Validación

Confiamos en entornos de desarrollo integrados (IDE) para ayudar con tareas como agregar, modificar y probar código nuevo, pero los IDE no pueden validar referencias de JavaScript puras. Mitigamos esta deficiencia al monitorear atentamente mientras codificamos para evitar la posibilidad de errores tipográficos en variables y nombres de funciones.

La magnitud del problema crece exponencialmente cuando el código se origina en un tercero, donde las referencias rotas en las ramas del código que rara vez se ejecutan fácilmente podrían pasar desapercibidas.

Por el contrario, con TypeScript, podemos centrar nuestros esfuerzos en la codificación, confiando en que cualquier error se identificará en el momento de la transpilación. Para demostrar esto, comencemos con un código JavaScript heredado:

 const moment = require('moment'); const printCurrentTime = (format) => { if (format === 'ISO'){ console.log("Current ISO TS:", moment().toISO()); } else { console.log("Current TS: ", moment().format(format)); } }

La llamada .toISO() es un error tipográfico del método moment.js toISOString() pero el código funcionaría, siempre que el argumento de format no sea ISO . La primera vez que intentemos pasar ISO a la función, generará este error de tiempo de ejecución: TypeError: moment(...).toISO is not a function .

Puede ser difícil localizar el código mal escrito. Es posible que el código base actual no tenga una ruta a la línea rota, en cuyo caso nuestra referencia rota .toISO() no sería detectada por la prueba.

Si portamos este código a TypeScript, el IDE resaltará la referencia rota, lo que nos pedirá que hagamos las correcciones. Si no hacemos nada e intentamos transpilar, estaríamos bloqueados y el transpilador generaría el siguiente error:

 TSError: ⨯ Unable to compile TypeScript: src/catching-mistakes-at-compile-time.ts:5:49 - error TS2339: Property 'toISO' does not exist on type 'Moment'. 5 console.log("Current ISO TS:", moment().toISO());

refactorización

Si bien los errores tipográficos en las referencias de código de terceros no son infrecuentes, hay un conjunto diferente de problemas asociados con los errores tipográficos en las referencias internas, como este:

 const myPhoneFunction = (opts) => { // ... if (opts.phoneNumbr) doStuff(); }

Un único desarrollador puede ubicar y corregir todas las instancias de phoneNumbr para terminar con er con bastante facilidad.

Pero cuanto más grande es el equipo, más costoso es este simple y común error. En el curso de la realización de su trabajo, los colegas deben ser conscientes de dichos errores tipográficos y propagarlos. Alternativamente, agregar código para admitir ambas ortografías inflaría la base de código innecesariamente.

Con TypeScript, cuando arreglamos un error tipográfico, el código dependiente ya no se transpilará, lo que indicará a los colegas que propaguen la corrección a su código.

Documentación

La documentación precisa y relevante es clave para la comunicación dentro y entre los equipos de desarrolladores. Los desarrolladores de JavaScript a menudo usan JSDoc para documentar el método esperado y los tipos de propiedades.

Las características del lenguaje de TypeScript (por ejemplo, clases abstractas, interfaces y definiciones de tipo) facilitan la programación de diseño por contrato, lo que lleva a una documentación de calidad. Además, tener una definición formal de los métodos y propiedades a los que debe adherirse un objeto ayuda a identificar cambios importantes, crear pruebas, realizar introspección de código e implementar patrones arquitectónicos.

Para TypeScript, la herramienta de acceso TypeDoc (basada en la propuesta de TSDoc) extrae automáticamente la información de tipo (por ejemplo, clase, interfaz, método y propiedad) de nuestro código. Por lo tanto, creamos sin esfuerzo una documentación que es, con mucho, más completa que la de JSDoc.

Ventajas de TypeScript frente a JavaScript

Ahora, exploremos cómo podemos usar TypeScript para abordar estos desafíos de escalabilidad.

Código avanzado/sugerencias de refactorización

Muchos IDE pueden procesar información del sistema de tipos TypeScript, proporcionando validación de referencia a medida que codificamos. Aún mejor, a medida que escribimos, el IDE puede entregar documentación relevante de un vistazo (por ejemplo, los argumentos que espera una función) para cualquier referencia y sugerir nombres de variables contextualmente correctos.

En este fragmento de TypeScript, el IDE sugiere un autocompletado de los nombres de las claves dentro del valor de retorno de la función:

 /** * Simple function to parse a CSV containing people info. * @param data A string containing a CSV with 3 fields: name, surname, age. */ const parsePeopleData = (data: string) => { const people: {name: string, surname: string, age: number}[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; const tokens = row.split(',').map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3){ errors.push(`Row "${row}" contains only ${tokens.length} tokens. 3 required`); continue; } people.push({ name: tokens[0], surname: tokens[1], age: +tokens[2] }) } return {people, errors}; }; const exampleData = ` Gordon,Freeman,27 G,Man,99 Alyx,Vance,24 Invalid Row,, Again, Invalid `; const result = parsePeopleData(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }

Mi IDE, Visual Studio Code, proporcionó esta sugerencia (en la llamada) cuando comencé a llamar a la función (línea 31):

En el momento de escribir parsePeopleData(), el IDE muestra una información sobre herramientas del transpilador de TypeScript que dice "parsePeopleData(datos: cadena): { personas: { nombre: cadena; apellido: cadena; edad: número; }[]; errores: string[]; }" seguido del texto contenido en el comentario de varias líneas antes de la definición de la función, "Una cadena que contiene un CSV con 3 campos: nombre, apellido, edad. Función simple para analizar un CSV que contiene información de personas".

Además, las sugerencias de autocompletar del IDE (en la llamada) son contextualmente correctas y muestran solo nombres válidos dentro de una situación clave anidada (línea 34):

Tres sugerencias (edad, nombre y apellido) que aparecieron en respuesta a escribir "mapa(p => `Nombre: ${p." La primera sugerencia está resaltada y tiene "(propiedad) edad: número" al lado.

Tales sugerencias en tiempo real conducen a una codificación más rápida. Además, los IDE pueden confiar en la información de tipo rigurosa de TypeScript para refactorizar el código en cualquier escala. Operaciones como cambiar el nombre de una propiedad, cambiar la ubicación de los archivos o incluso extraer una superclase se vuelven triviales cuando estamos 100% seguros de la precisión de nuestras referencias.

Soporte de interfaz

A diferencia de JavaScript, TypeScript ofrece la posibilidad de definir tipos mediante interfaces . Una interfaz enumera formalmente, pero no implementa, los métodos y propiedades que debe incluir un objeto. Esta construcción de lenguaje es particularmente útil para la colaboración con otros desarrolladores.

El siguiente ejemplo destaca cómo podemos aprovechar las funciones de TypeScript para implementar patrones comunes de programación orientada a objetos (OOP, en este caso, estrategia y cadena de responsabilidad ), mejorando así el ejemplo anterior:

 export class PersonInfo { constructor( public name: string, public surname: string, public age: number ){} } export interface ParserStrategy{ /** * Parse a line if able. * @returns The parsed line or null if the format is not recognized. */ (line: string): PersonInfo | null; } export class PersonInfoParser{ public strategies: ParserStrategy[] = []; parse(data: string){ const people: PersonInfo[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; let parsed; for (let s of this.strategies){ parsed = s(row); if (parsed) break; } if (!parsed){ errors.push(`Unable to find a strategy capable of parsing "${row}"`); } else { people.push(parsed); } } return {people, errors}; } } const exampleData = ` Gordon,Freeman,27 G;Man;99 {"name":"Alyx", "surname":"Vance", "age":24} Invalid Row,, Again, Invalid `; const parser = new PersonInfoParser(); const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => { const tokens = line.split(fieldSeparator).map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3) return null; return new PersonInfo(tokens[0], tokens[1], +tokens[2]); }; parser.strategies.push( (line) => { try { const {name, surname, age} = JSON.parse(line); return new PersonInfo(name, surname, age); } catch(err){ return null; } }, createCSVStrategy(), createCSVStrategy(';') ); const result = parser.parse(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }

Módulos ES6: en cualquier lugar

Al momento de escribir este artículo, no todos los tiempos de ejecución de JavaScript de front-end y back-end son compatibles con los módulos ES6. Sin embargo, con TypeScript, podemos usar la sintaxis del módulo ES6:

 import * as _ from 'lodash'; export const exampleFn = () => console.log(_.reverse(['a', 'b', 'c']));

La salida transpilada será compatible con nuestro entorno seleccionado. Por ejemplo, usando la opción del compilador --module CommonJS , obtenemos:

 "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn;

Usando --module UMD en su lugar, TypeScript genera el patrón UMD más detallado:

 (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "lodash"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; });

Clases de ES6: en cualquier lugar

Los entornos heredados a menudo carecen de compatibilidad con las clases de ES6. Una transpila de TypeScript garantiza la compatibilidad mediante el uso de construcciones específicas de destino. Aquí hay un fragmento de fuente de TypeScript:

 export class TestClass { hello = 'World'; }

La salida de JavaScript depende tanto del módulo como del destino, que TypeScript nos permite especificar.

Esto es lo que --module CommonJS --target es3 :

 "use strict"; exports.__esModule = true; exports.TestClass = void 0; var TestClass = /** @class */ (function () { function TestClass() { this.hello = 'World'; } return TestClass; }()); exports.TestClass = TestClass;

Usando --module CommonJS --target es6 en su lugar, obtenemos el siguiente resultado transpilado. La palabra clave de class se utiliza para apuntar a ES6:

 "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestClass = void 0; class TestClass { constructor() { this.hello = 'World'; } } exports.TestClass = TestClass;

Funcionalidad Async/Await: en cualquier lugar

Async/await hace que el código JavaScript asíncrono sea más fácil de entender y mantener. TypeScript ofrece esta funcionalidad a todos los tiempos de ejecución, incluso a aquellos que no proporcionan async/await de forma nativa.

Tenga en cuenta que para ejecutar async/await en tiempos de ejecución más antiguos como ES3 y ES5, necesitará soporte externo para la salida basada en Promise (por ejemplo, a través de Bluebird o un polyfill ES2015). El polyfill de Promise que se incluye con TypeScript se integra fácilmente en la salida transpilada; solo necesitamos configurar la opción del compilador lib en consecuencia.

Compatibilidad con campos de clases privadas, en cualquier lugar

Incluso para destinos heredados, TypeScript admite campos private de la misma manera que los lenguajes fuertemente tipados (por ejemplo, Java o C#). Por el contrario, muchos tiempos de ejecución de JavaScript admiten campos private a través de la sintaxis de prefijo hash, que es una propuesta final de ES2022.

Desventajas de TypeScript frente a JavaScript

Ahora que hemos resaltado los principales beneficios de implementar TypeScript, exploremos escenarios en los que TypeScript puede no ser la opción adecuada.

Transpilación: Potencial de incompatibilidad de flujo de trabajo

Los flujos de trabajo específicos o los requisitos del proyecto pueden ser incompatibles con el paso de transpilación de TypeScript: por ejemplo, si necesitamos usar una herramienta externa para cambiar el código después de la implementación o si el resultado generado debe ser apto para desarrolladores.

Por ejemplo, recientemente escribí una función AWS Lambda para un entorno Node.js. TypeScript no encajaba bien porque la transpilación nos impediría a mí y a otros miembros del equipo editar la función con el editor en línea de AWS. Esto fue un factor decisivo para el gerente del proyecto.

Tipo El sistema funciona solo hasta el tiempo de transpilación

La salida de JavaScript de TypeScript no contiene información de tipo, por lo que no realizará comprobaciones de tipo y, por lo tanto, la seguridad de tipo puede romperse en tiempo de ejecución. Por ejemplo, supongamos que una función está definida para devolver siempre un objeto. Si se devuelve null por su uso dentro de un archivo .js , se producirá un error de tiempo de ejecución.

Las características que dependen de la información de tipo (p. ej., campos privados, interfaces o genéricos) agregan valor a cualquier proyecto, pero se eliminan durante la transpilación. Por ejemplo, los miembros de la clase private ya no serían privados después de la transpilación. Para ser claros, los problemas de tiempo de ejecución de esta naturaleza no son exclusivos de TypeScript, y también puede esperar encontrar las mismas dificultades con JavaScript.

Combinando TypeScript y JavaScript

A pesar de los muchos beneficios de TypeScript, a veces no podemos justificar la conversión de un proyecto de JavaScript completo de una sola vez. Afortunadamente, podemos especificarle al transpilador de TypeScript, archivo por archivo, qué interpretar como JavaScript simple. De hecho, este enfoque híbrido puede ayudar a mitigar los desafíos individuales a medida que surgen en el transcurso del ciclo de vida de un proyecto.

Es posible que prefiramos dejar JavaScript sin cambios si el código:

  • Fue escrito por un antiguo colega y requeriría importantes esfuerzos de ingeniería inversa para convertirlo a TypeScript.
  • Usa técnicas no permitidas en TypeScript (p. ej., agrega una propiedad después de la instanciación del objeto) y requeriría una refactorización para cumplir con las reglas de TypeScript.
  • Pertenece a otro equipo que sigue usando JavaScript.

En tales casos, un archivo de declaración (archivo .d.ts , a veces llamado archivo de definición o archivo de tipos) le da a TypeScript suficientes datos de tipo para habilitar las sugerencias de IDE mientras deja el código JavaScript tal como está.

Muchas bibliotecas de JavaScript (p. ej., Lodash, Jest y React) proporcionan archivos de escritura TypeScript en paquetes de tipos independientes, mientras que otras (p. ej., Moment.js, Axios y Luxon) integran archivos de escritura en el paquete principal.

TypeScript vs. JavaScript: una cuestión de racionalización y escalabilidad

El soporte, la flexibilidad y las mejoras inigualables que están disponibles a través de TypeScript mejoran significativamente la experiencia del desarrollador, lo que permite escalar proyectos y equipos. El costo principal de incorporar TypeScript en un proyecto es la adición del paso de compilación de transpilación. Para la mayoría de las aplicaciones, la transpilación a JavaScript no es un problema; más bien, es un trampolín hacia los muchos beneficios de TypeScript.


Lecturas adicionales en el blog de ingeniería de Toptal:

  • Trabajar con compatibilidad con TypeScript y Jest: un tutorial de AWS SAM