TypeScript

Mejora tu inversión en JavaScript con TypeScript

Bill Wagner

Descargar el código de ejemplo

El lenguaje de programación TypeScript es actualmente un superconjunto completo de JavaScript. Si usas JavaScript, ya escribes en TypeScript. Esto no quiere decir que escribas bien en TypeScript y aproveches todas sus características. Sí quiere decir que la ruta de migración de tu inversión en JavaScript existente al código base de TypeScript no será tan difícil y que aprovechará las nuevas características que ofrece TypeScript.

En este artículo, daré recomendaciones sobre la migración de una aplicación de JavaScript a TypeScript. Aprenderás a mover de JavaScript a TypeScript para usar el sistema de tipos de TypeScript para escribir códigos mejores. Con el análisis estático de TypeScript, podrás minimizar los errores y ser más productivo. Al seguir estas recomendaciones, también podrás minimizar la cantidad de errores y advertencias del sistema de tipos de TypeScript durante la migración.

Comenzaré con un ejemplo de una aplicación que administre una libreta de direcciones. Es una aplicación de una sola página (SPA) que usa JavaScript en el cliente. A los efectos de este artículo, el ejemplo es sencillo y solo incluí la parte que muestra una lista de contactos. Usa un marco Angular para el enlace de datos y el soporte de la aplicación. El marco Angular controla el enlace de datos y las plantillas para mostrar la información de contactos.

Tres archivos de JavaScript componen la aplicación: El archivo app.js contiene el código que inicia la aplicación. El archivo contactsController.js es el controlador de la página de enumeración. El archivo contactsData.js contiene una lista de contactos que se mostrará. El controlador, junto con el marco Angular, controla el comportamiento de la página de enumeración. Se pueden ordenar los contactos y mostrar u ocultar los detalles de cualquier contacto individual. El archivo contactsData.js es un conjunto de contactos codificado de forma rígida. En una aplicación de producción, el archivo contendría código para llamar al servidor y recuperar datos. La lista de contactos codificada de forma rígida ofrece una demo más autocontenida.

No te preocupes si no tienes tanta experiencia con Angular. Verás lo fácil que es usarlo cuando comience a migrar la aplicación. La aplicación sigue las convenciones de Angular, que son fáciles de preservar al migrar una aplicación a TypeScript.

El mejor lugar para comenzar a migrar una aplicación a TypeScript es el archivo del controlador. Dado que cualquier código válido de JavaScript es también un código válido de TypeScript, simplemente cambia la extensión del archivo del controlador llamado contactsController.js de .js a .ts. El lenguaje TypeScript es un ciudadano de primer nivel en Visual Studio 2013 Update 2. Si tienes instalada la extensión Web Essentials, verás tanto el origen de TypeScript como la salida de JavaScript generada en la misma ventana (ver figura 1).

The TypeScript Editing Experience in Visual Studio 2013 Update 2
Figura 1 La experiencia de edición de TypeScript en Visual Studio 2013 Update 2

Dado que aún no se usan las características de lenguaje específicas de TypeScript, las dos vistas son casi iguales. La línea de comentario adicional al final da información a Visual Studio cuando se depuran aplicaciones TypeScript. Con Visual Studio, una aplicación se puede depurar en el nivel de TypeScript, en lugar de hacerlo en el nivel de origen de JavaScript generado.

Se puede ver que el compilador de TypeScript informa de un error para esta aplicación, aunque el compilador genere una salida de JavaScript válida. Esa es una de las grandes características del lenguaje TypeScript. Es una consecuencia natural de esta regla que TypeScript sea un superconjunto estricto de JavaScript. Aún no declaré el símbolo contactsApp en ningún archivo de TypeScript. Por lo tanto, el compilador de TypeScript supone el tipo any y supone que el símbolo hará referencia a un objeto en tiempo de ejecución. A pesar de estos errores, puedo ejecutar la aplicación y funcionará correctamente.

Puedo seguir y cambiar las extensiones de todos los archivos de JavaScript en la aplicación. Pero no recomiendo hacerlo todavía, porque habría muchos más errores. La aplicación aún funcionaría, pero con tantos errores sería más difícil usar el sistema TypeScript para escribir códigos mejores. Yo prefiero trabajar en un archivo a la vez y agregar la información del tipo a la aplicación a medida que avanzo. De este modo, hay menos errores del sistema de tipos que corregir de una sola vez. Después de tener una compilación limpia, sé que el compilador de TypeScript me ayudará a evitar esos errores.

Resulta fácil declarar una variable externa para contactsApp. De forma predeterminada, tendría el tipo any:

declare var contactsApp: any;

Si bien esto corrige el error del compilador, no ayuda a evitar los errores al llamar a métodos en la biblioteca de Angular. El tipo any es justo como su nombre indica: podría ser cualquier cosa. TypeScript no hará ninguna comprobación de tipos cuando tenga acceso a la variable contactsApp. Para la comprobación de tipos, debes informar a TypeScript sobre el tipo de contactsApp y sobre los tipos definidos en el marco Angular.

TypeScript habilita la información de tipos para las bibliotecas de JavaScript existentes con una característica llamada Definiciones de tipos. Una Definición de tipos es un conjunto de declaraciones sin ninguna implementación. Describe los tipos y sus API al compilador de TypeScript. El proyecto DefinitelyTyped en GitHub tiene definiciones de tipos para muchas bibliotecas populares de JavaScript, incluida Angular.js. Incluyo estas definiciones en el proyecto con el administrador de paquetes NuGet.

Una vez incluidas las Definiciones de tipos para la biblioteca Angular, puedo usarlas para corregir los errores que veo en el compilador. Tengo que hacer referencia a la información de tipos que acabo de agregar al proyecto. Hay un comentario especial que le dice al compilador de TypeScript que haga referencia a la información de tipos:

/// <reference path="../Scripts/typings/angularjs/angular.d.ts" />

El compilador de TypeScript ahora puede interpretar cualquiera de los tipos definidos en el archivo de Definición de tipos angular.d.ts. Es hora de corregir el tipo de la variable contactsApp. El tipo esperado de la variable contactsApp, que se declara en app.js en el espacio de nombres ng, es un IModule:

declare var contactsApp: ng.IModule;

Con esta declaración, obtendré IntelliSense cada vez que se pulse un punto después de contactsApp. También voy a obtener informes de errores del compilador de TypeScript cuando escriba o use indebidamente las API declaradas en el objeto contactsApp. Los errores del compilador ya no están y he incluido la información de tipos estáticos para el objeto app.

Al resto del código en el objeto contactsController aún le falta la información de tipos. Hasta que no agregues las anotaciones de tipos, el compilador de TypeScript supondrá que las variables son de tipo any. El segundo parámetro del método contactsApp.controller es una función, y el primer parámetro de esa función, $scope, es de tipo ng.IScope. Así que incluiré ese tipo en la declaración de la función (contactData aún se interpretará como del tipo any):

contactsApp.controller('ContactsController', function ContactsController($scope : ng.IScope, contactData) { $scope.sortOrder = 'last'; $scope.hideMessage = "Hide Details"; $scope.showMessage = "Show Details"; $scope.contacts = contactData.getContacts(); $scope.toggleShowDetails = function (contact) { contact.showDetails = !contact.showDetails; } });

Esto introduce un nuevo conjunto de errores del compilador. Los nuevos errores se deben a que el código dentro de esa función contactsController manipula propiedades que no son parte del tipo ng.IScope. Ng.IScope es una interfaz, y el objeto $scope real es un tipo específico de la aplicación que implementa IScope. Las propiedades manipuladas son miembros de ese tipo. Para aprovechar los tipos estáticos de TypeScript, debo definir ese tipo específico de la aplicación. Lo llamaré IContactsScope:

interface IContactsScope extends ng.IScope { sortOrder: string; hideMessage: string; showMessage: string; contacts: any; toggleShowDetails: (contact: any) => boolean; }

Una vez definida la interfaz, simplemente cambio el tipo de la variable $scope en la declaración de la función:

function ContactsController($scope : IContactsScope, contactData) {

Después de hacer estos cambios, puedo compilar la aplicación sin errores y se ejecutará correctamente. Hay varios conceptos importantes que se deben cumplir al agregar esta interfaz. Observa que no tuve que buscar ningún otro código y declarar que ningún tipo en particular implementa el tipo IContactsScope. TypeScript admite los tipos estructurales, llamados “duck typing”. Es decir, cualquier objeto que declara las propiedades y los métodos declarados en IContactsScope implementa la interfaz de IContactsScope, ya sea que ese tipo declare que implementa IContactsScope o no.

Observa que estoy usando el tipo any de TypeScript como marcador de posición en la definición de IContactsScope. La propiedad contacts representa la lista de contactos y aún no he migrado el tipo Contact. Puedo usar any como marcador de posición y el compilador de TypeScript no realizará ninguna comprobación de tipos al acceder a esos valores. Esta es una técnica útil durante toda la migración de una aplicación.

El tipo any representa cualquier tipo que no haya migrado de JavaScript a TypeScript. Hace que la migración sea más sencilla y haya que solucionar menos errores del compilador de TypeScript en cada iteración. También puedo buscar variables declaradas como tipo any y encontrar tareas que queden por cumplir. “Any” le dice al compilador de TypeScript que no realice ninguna comprobación de tipos en esa variable. Podría ser cualquier cosa. El compilador supondrá que conoce las API disponibles en esa variable. Esto no significa que todo uso de “any” sea malo. Hay usos válidos del tipo any, como cuando una API JavaScript se diseña para funcionar con distintos tipos de objetos. Usar “any” como marcador de posición durante una migración es una buena forma.

Finalmente, la declaración de toggleShowDetails muestra cómo se representan las declaraciones de funciones en TypeScript:

toggleShowDetails: (contact: any) => boolean;

El nombre de la función es toggleShowDetails. Después de los dos puntos, verás una lista de parámetros. Esta función toma un solo parámetro, actualmente del tipo any. El nombre “contact” es opcional. Puedes usarlo para dar más información a otros programadores. La flecha gruesa apunta al tipo de retorno, que es un booleano en este ejemplo.

Haber introducido el tipo any en la definición de IContactScope te muestra adónde ir para seguir trabajando. TypeScript te ayuda a evitar errores cuando le das más información sobre los tipos con los que trabajas. Voy a sustituir ese any por una mejor definición de lo que hay en un Contact al definir un tipo IContact que incluya las propiedades disponibles en un objeto contact (ver figura 2).

Figura 2 Inclusión de propiedades en un objeto contact

interface IContact { first: string; last: string; address: string; city: string; state: string; zipCode: number; cellPhone: number; homePhone: number; workPhone: number; showDetails: boolean }

Con la interfaz IContact ahora definida, la voy a usar en la interfaz de IContactScope:

interface IContactsScope extends ng.IScope { sortOrder: string; hideMessage: string; showMessage: string; contacts: IContact[]; toggleShowDetails: (contact: IContact) => boolean; }

No tengo que agregar la información de tipos a la definición de la función toggleShowDetails definida en la función contactsController. Dado que la variable $scope es un IContactsScope, el compilador de TypeScript sabe que la función asignada a toggleShowDetails debe coincidir con el prototipo de función definido en IContactScope y que el parámetro debe ser un IContact.

Observa el JavaScript generado para esta versión de contactsController en la figura 3. Observa cómo todos los tipos de la interfaz que he definido se quitaron del JavaScript generado. Las anotaciones de tipos existen para ti y para las herramientas de análisis estático. Estas anotaciones no se transmiten al JavaScript generado porque no son necesarias.

Figura 3 La versión de TypeScript del controlador y el JavaScript generado

/// reference path="../Scripts/typings/angularjs/angular.d.ts" var contactsApp: ng.IModule; interface IContact { first: string; last: string; address: string; city: string; state: string; zipCode: number; cellPhone: number; homePhone: number; workPhone: number; showDetails: boolean } interface IContactsScope extends ng.IScope { sortOrder: string; hideMessage: string; showMessage: string; contacts: IContact[]; toggleShowDetails: (contact: IContact) => boolean; } contactsApp.controller('ContactsController', function ContactsController($scope : IContactsScope, contactData) { $scope.sortOrder = 'last'; $scope.hideMessage = "Hide Details"; $scope.showMessage = "Show Details"; $scope.contacts = contactData.getContacts(); $scope.toggleShowDetails = function (contact) { contact.showDetails = !contact.showDetails; return contact.showDetails; } }); // Generated JavaScript /// reference path="../Scripts/typings/angularjs/angular.d.ts" var contactsApp; contactsApp.controller('ContactsController', function ContactsController($scope, contactData) { $scope.sortOrder = 'last'; $scope.hideMessage = "Hide Details"; $scope.showMessage = "Show Details"; $scope.contacts = contactData.getContacts(); $scope.toggleShowDetails = function (contact) { contact.showDetails = !contact.showDetails; return contact.showDetails; }; }); //# sourceMappingURL=contactsController.js.map

Agregar definiciones de módulo y de clase

Agregar anotaciones de tipos al código permite que las herramientas de análisis estático encuentren e informen de posibles errores que has introducido en el código. Esto abarca todo, desde IntelliSense, pasando por el análisis similar a lint, hasta los errores y las advertencias de tiempo de compilación.

Otra gran ventaja que ofrece TypeScript en comparación con JavaScript es una mejor sintaxis para abarcar tipos. La palabra clave del módulo de TypeScript te permite ubicar definiciones de tipos dentro de un ámbito y evitar colisiones con tipos de otros módulos que puedan usar el mismo nombre.

La aplicación de muestra de contactos no es tan grande, pero sigue siendo conveniente colocar definiciones de tipos en el módulo para evitar colisiones. Aquí, colocaré el contactsController y los otros tipos que he definido dentro del módulo llamado Rolodex:

module Rolodex { // Elided }

En este módulo, no he agregado la palabra clave de exportación a ninguna definición. Es decir, solo se puede hacer referencia a los tipos definidos dentro del módulo Rolodex desde el interior de ese módulo. Voy a agregar la palabra clave de exportación a las interfaces definidas en este módulo y a usar esos tipos más adelante, cuando migre el código de contactsData. También voy a cambiar el código del ContactsController desde una función a una clase. Esta clase necesita un constructor para inicializarse, pero ningún otro método público (ver figura 4).

Figura 4 Cambio de ContactsController de función a clase

export class ContactsController { constructor($scope: IContactsScope, contactData: any) { $scope.sortOrder = 'last'; $scope.hideMessage = "Hide Details"; $scope.showMessage = "Show Details"; $scope.contacts = contactData.getContacts(); $scope.toggleShowDetails = function (contact) { contact.showDetails = !contact.showDetails; return contact.showDetails; } } }

Crear este tipo ahora cambia la llamada a contactsApp.controller. El segundo parámetro es ahora el tipo de clase, no la función definida anteriormente. El primer parámetro de la función del controlador es el nombre del controlador. Angular asigna los nombres del controlador a funciones del constructor. En cualquier lugar de la página HTML donde se haga referencia al tipo ContactsController, Angular llamará al constructor para la clase ContactsController:

contactsApp.controller('ContactsController', Rolodex.ContactsController);

Ahora, el tipo del controlador se ha migrado por completo de JavaScript a TypeScript. La nueva versión contiene anotaciones de tipos para todo lo que se define o usa en el controlador. En TypeScript, podría hacer lo mismo sin necesidad de cambios en las otras partes de la aplicación. Ningún otro archivo se vio afectado. Mezclar TypeScript con JavaScript no presenta problemas, lo que simplifica agregar TypeScript a una aplicación JavaScript existente. El sistema de tipos de TypeScript se basa en la interfaz de tipos y en los tipos estructurales, lo que facilita la interacción entre TypeScript y JavaScript.

Ahora, voy a pasar al archivo contactData.js (ver figura 5). Esta función usa el método de fábrica de Angular para devolver un objeto que devuelve una lista de contactos. Al igual que el controlador, el método de fábrica asigna nombres (contactData) a una función que devuelve el servicio. Esta conversión se usa en el constructor del controlador. El segundo parámetro del constructor se llama contactData. Angular usa este nombre de parámetro para la asignación a la fábrica correcta. Como ves, el marco de Angular se basa en convenciones.

Figura 5 La versión de JavaScript del servicio contactData

'use strict'; contactsApp.factory('contactData', function () { var contacts = [ { first: "Tom", last: "Riddle", address: "66 Shack St", city: "Little Hangleton", state: "Mississippi", zipCode: 54565, cellPhone: 6543654321, homePhone: 4532332133, workPhone: 6663420666 }, { first: "Antonin", last: "Dolohov", address: "28 Kaban Ln", city: "Gideon", state: "Arkansas", zipCode: 98767, cellPhone: 4443332222, homePhone: 5556667777, workPhone: 9897876765 }, { first: "Evan", last: "Rosier", address: "28 Dominion Ave", city: "Notting", state: "New Jersey", zipCode: 23432, cellPhone: 1232343456, homePhone: 4432215565, workPhone: 3454321234 } ]; return { getContacts: function () { return contacts; }, addContact: function(contact){ contacts.push(contact); return contacts; } }; })

De nuevo, el primer paso es simplemente cambiar la extensión de .js a .ts. Se compila sin problemas, y el JavaScript generado se parece mucho al archivo TypeScript de origen. Luego, voy a poner el código en el archivo contactData.ts en el mismo módulo Rolodex. Eso abarca todo el código de la aplicación en la misma partición lógica.

Luego, voy a migrar la fábrica contactData a una clase. Declara la clase como de tipo ContactDataServer. En lugar de una función que devuelve un objeto con dos propiedades que son los métodos, ahora simplemente puedo definir los métodos como miembros de un objeto de ContactDataServer. Los datos iniciales ahora son un miembro de datos de un objeto de tipo ContactDataServer. También necesito usar este tipo en la llamada a contactsApp.factory:

contactsApp.factory('contactsData', () => new Rolodex.ContactDataServer());

El segundo parámetro es una función que devuelve un nuevo Contact­DataServer. La fábrica creará el objeto cuando yo lo necesite. Si intento compilar y ejecutar esta versión, se producirán errores del compilador porque el tipo ContactDataServer no se exporta desde el módulo Rolodex. Pero se hace referencia a él en la llamada a contacts­App.factory. Este es otro ejemplo de cómo el sistema de tipos de TypeScript es tan compasivo, lo que facilita mucho las tareas de migración. Puedo corregir fácilmente este error si agrego la palabra clave de exportación a la declaración de la clase ContactDataServer.

En la figura 6 se puede ver la versión final. Observa que he agregado la información de tipos para la matriz de contactos y el parámetro de entrada en el método addContact. Las anotaciones de tipos son opcionales; TypeScript es válido sin ellas. Pero recomiendo agregar toda la información de tipos necesaria al código TypeScript porque ayuda a evitar errores en el sistema de TypeScript, lo que te permitirá ser más productivo.

Figura 6 La versión de TypeScript de ContactDataServer

/// reference path="../Scripts/typings/angularjs/angular.d.ts" var contactsApp: ng.IModule; module Rolodex { export class ContactDataServer { contacts: IContact[] = [ { first: "Tom", last: "Riddle", address: "66 Shack St", city: "Little Hangleton", state: "Mississippi", zipCode: 54565, cellPhone: 6543654321, homePhone: 4532332133, workPhone: 6663420666, showDetails: true }, { first: "Antonin", last: "Dolohov", address: "28 Kaban Ln", city: "Gideon", state: "Arkansas", zipCode: 98767, cellPhone: 4443332222, homePhone: 5556667777, workPhone: 9897876765, showDetails: true }, { first: "Evan", last: "Rosier", address: "28 Dominion Ave", city: "Notting", state: "New Jersey", zipCode: 23432, cellPhone: 1232343456, homePhone: 4432215565, workPhone: 3454321234, showDetails: true } ]; getContacts() { return this.contacts; } addContact(contact: IContact) { this.contacts.push(contact); return this.contacts; } } } contactsApp.factory('contactsData', () => new Rolodex.ContactDataServer());

Ahora que he creado la nueva clase ContactDataServer, puedo hacer un último cambio en el controlador. Recuerda que el segundo parámetro del constructor de contactsController era el servidor de datos. Ahora puedo aumentar la seguridad de tipos al declarar que el parámetro siguiente debe ser de tipo ContactDataServer:

constructor($scope: IContactsScope, contactData: ContactDataServer) {

Migraciones sin problemas de JavaScript a TypeScript

TypeScript tiene muchas más características que las demostradas aquí. A medida que trabajes en TypeScript, adoptarás sus capacidades. Cuanto más uses las extensiones de TypeScript para JavaScript, más aumentará tu productividad. Recuerda que las anotaciones de tipos de TypeScript se han diseñado para ofrecer una migración sin problemas de JavaScript a TypeScript. Lo que es más importante, recuerda que TypeScript es un superconjunto estricto de JavaScript. Esto significa que cualquier código JavaScript válido es válido en TypeScript.

Además, las anotaciones de tipos de TypeScript tienen escasa formalidad. Las anotaciones de tipos se comprueban en el lugar donde las agregas y no estás obligado a agregarlas en ningún lugar. Al migrar de JavaScript a TypeScript, resulta muy útil.

Finalmente, el sistema de tipos de TypeScript admite los tipos estructurales. Al definir interfaces para tipos importantes, el sistema de tipos de TypeScript supone que cualquier objeto con esos métodos y propiedades admitirá esa interfaz. No es necesario declarar la compatibilidad de la interfaz en la definición de cada clase. Los objetos anónimos también pueden admitir interfaces con esta característica de tipos estructurales.

Estas características combinadas crean una ruta despejada para migrar el código base de JavaScript a TypeScript. Cuanto más avances en la ruta de migración, mayores serán las ventajas que obtendrás del análisis de códigos estáticos de TypeScript. El objetivo final debe ser aprovechar la mayor seguridad posible de TypeScript. En el camino, el código JavaScript existente funciona como TypeScript válido que no usa las anotaciones de tipos de TypeScript. Es un proceso que casi no genera fricción. No tienes ningún motivo para no usar TypeScript en tus aplicaciones JavaScript actuales.

Bill Wagner es el autor del best-seller, “Effective C#” (C# eficaz, 2004), ahora en su segunda edición, y “More Effective C#” (Un C# más eficaz, 2008), ambos de Addison-Wesley Professional. También ha publicado los videos para Pearson Education informIT, “C# Async Fundamentals LiveLessons” y “C# Puzzlers”. Publica en los blogs thebillwagner.com. Para comunicarte con él, escribe a bill.w.wagner@outlook.com.

Gracias al siguiente experto técnico de Microsoft por su ayuda en la revisión de este artículo: Jonathan Turner
Jonathan Turner es el jefe de programas del equipo de TypeScript en Microsoft y codiseñador del lenguaje TypeScript. Antes de unirse a Microsoft, trabajó en Clang/LLVM y en el lenguaje de programación Chapel.