LogoCésar Alberca
- 15 minutos

Migrando a TypeScript

Diseña Mejor Arquitectura Frontend
Patrones de Arquitectura Frontend complejos explicados de forma sencilla. Envíos recurrentes y directos a tu correo. Conviértete en un mejor Frontend Architect.
Respeto tu privacidad. Cancela en cualquier momento.

La newsletter está escrita en inglés.

En este tutorial veremos cómo migrar una base de código de JavaScript a TypeScriptSe abre en una nueva pestaña. Migraremos el código de mi charla Patrones Avanzados de JavaScript (vaya ironía, ¿verdad?). Aquí tienes el repositorioSe abre en una nueva pestaña. También puedes ver mi charla aquíSe abre en una nueva pestaña. ¡Empecemos!

#Configuración

Lo primero de todo es incluir un archivo tsconfig.json en la raíz de tu proyecto. En este archivo estableceremos las opciones para TypeScript:

{ "compilerOptions": { "moduleResolution": "node", "outDir": "./dist", "allowJs": true, "target": "esnext" }, "include": ["./src/**/*"], "exclude": ["node_modules"] }

moduleResolution especifica cómo TypeScript va a resolver las dependenciasSe abre en una nueva pestaña.

outDir será el directorio en el que se compilará el código TypeScript. Este será el código que desplegaremos o ejecutaremos.

¡La opción allowJs nos permite hacer una migración incremental, lo cual es genial!

target nos permite establecer la versión de JavaScript a la que queremos compilar. Incluso podríamos establecerlo a la versión es3 haciendo que nuestro código sea ejecutable en navegadores antiguos.

Por último, con include, definimos qué rutas debe comprobar el compilador de TypeScript. Por convención, deberíamos tener nuestro código en el directorio src.

#Instalación

Ahora instalamos TypeScript como una dev-dependency: npm i typescript -DE. Añadimos a los scripts del package.json el script de compilación:

{ "scripts": { "compile": "tsc --pretty" } }

Como no tenemos ningún archivo .ts que podamos compilar, veremos que la compilación es exitosa:

image-20191112213355655

#Migración

Intentemos cambiar la extensión de .js a .ts y compilemos de nuevo:

image-20191112213602568

¡¿Qué?! ¡Compila correctamente! Pues sí, cualquier archivo JavaScript es código TypeScript válido.

TypeScript por defecto infiere cualquier cosa que no entiende como anySe abre en una nueva pestaña. any no mola, en absoluto. Con la opción noImplicitAny establecida en true en nuestro tsconfig.json, obtendremos un error en los lugares donde TypeScript infiera algo como any.

{ "compilerOptions": { "noImplicitAny": true } }

Ahora, cuando compilemos, empezaremos a ver algunos errores. Para obtener plenamente las ventajas de TypeScript como: seguridad, refactorizaciones y una mejor DX (experiencia de desarrollo), necesitamos ayudarle indicando los tipos que usamos, y eso es lo que vamos a hacer:

export function multiply(a = 1, b = 1) { return a * b } export function priceAfterTaxes(price, tax = multiply(21, 0.01)) { return price + price * tax }

Se convertirá en:

export function multiply(a = 1, b = 1) { return a * b } export function priceAfterTaxes(price: number, tax = multiply(21, 0.01)) { return price + price * tax }

Aquí vemos que TypeScript no necesita que especifiquemos que a o b en la función multiply son numbers dado que es lo suficientemente inteligente como para inferirlo. Además, si recibe dos parámetros que son números, y los multiplicamos, ¿cuál sería el tipo de retorno? Pues obviamente number.

#Never

Revisemos el archivo propValidator:

export function isRequired() { throw new TypeError('Argument is required') } export function capitalize(string = isRequired()) { return string.toUpperCase().slice(0, 1) + string.slice(1) }

Se convierte en:

export function isRequired(): never { throw new TypeError('Argument is required') } export function capitalize(string: string = isRequired()) { return string.toUpperCase().slice(0, 1) + string.slice(1) }

never es un tipo muy interesante que especifica algo que nunca va a cambiar. No es muy común usarlo, pero en este caso esta función nunca devuelve nada. Aquí vemos que TypeScript se adapta a la gran flexibilidad que ofrece JavaScript. Es en muy pocos casos donde no podemos modelar un programa JavaScript en TypeScript y siempre existe la salida de escape any.

Si quieres, puedes echar un vistazo a los tipos básicos en la documentación oficialSe abre en una nueva pestaña

#Clases

Con las clases, vemos ciertas ventajas respecto a sus contrapartes en JavaScript:

export class Subject { constructor() { this.observers = [] this._counter = 0 } set counter(value) { // https://github.com/tc39/proposal-private-fields this._counter = value this.notifyObservers() } get counter() { return this._counter } incrementCounter() { this.counter++ this.notifyObservers() } addObserver(observer) { this.observers.push(observer) } notifyObservers() { this.observers.forEach(observer => observer.notify(this)) } }

En TypeScript es:

import { Observer } from './Observer' export class Subject { private _counter = 0 private readonly observers: Observer[] constructor() { this.observers = [] } set counter(value) { // https://github.com/tc39/proposal-private-fields this._counter = value this.notifyObservers() } get counter() { return this._counter } incrementCounter() { this.counter++ this.notifyObservers() } addObserver(observer: Observer) { this.observers.push(observer) } notifyObservers() { this.observers.forEach(observer => observer.notify(this)) } }

Aquí ya no necesitamos usar la convención _ para especificar que una propiedad o método es privado. Con TypeScript, tenemos disponible la privacidadSe abre en una nueva pestaña con la palabra clave private –también tenemos protected y public–. Por defecto, si no especificamos nada, es public.

Tampoco necesitamos declarar los tipos y hacer las asignaciones en el constructor como lo hacemos en el archivo Observer:

export class Observer { constructor(subject) { this.subject = subject this.subject.addObserver(this) this.value = 0 } display() { return `Observer counter: ${this.value}` } notify(subject) { this.value = subject.counter } }

Podemos hacerlo de la siguiente manera:

import { Subject } from './Subject' export class Observer { private value = 0 constructor(private readonly subject: Subject) { this.subject.addObserver(this) } display() { return `Observer counter: ${this.value}` } notify(subject: Subject) { this.value = subject.counter } }

Además, si en el constructor especificamos private o public:

class Foo { foo: number constructor(foo: number) { this.foo = foo } }

Es lo mismo que esto:

class Foo { constructor(public foo: number) {} }

#Genéricos

Veamos un ejemplo más complejo:

// Más adelante veremos cómo podemos mejorar el tipo de retorno any export function createSafe(target: object): any { const handler = { get(target: object, name: string | number | symbol, receiver: any) { if (hasKey(target, name)) { const targetElement = Reflect.get(target, name, receiver) if (isObject(targetElement)) { return createSafe(targetElement) } return targetElement } return notDefined }, } return new Proxy(target, handler) } export const notDefined: object = new Proxy( {}, { get() { return notDefined }, }, ) export const either = (value: object, fallback: any) => (value === notDefined ? fallback : value) const isObject = (obj: object) => typeof obj === 'object' const hasKey = (obj: object, key: string | number | symbol) => key in obj

Mejoremos los tipos, porque estamos haciendo uso de any y no deberíamos. Dada la naturaleza de los proxiesSe abre en una nueva pestaña que estamos creando, el tipo de retorno es el mismo que el tipo del elemento que estamos pasando a través del parámetro de la función. Ahora mismo, como lo estamos marcando como any, podríamos cometer errores y TypeScript no podría decírnoslo:

const object = createSafe({ foo: 'bar' }) object.propertyThatDoesNotExist // OK

Así que tendríamos un error en tiempo de ejecución porque no pudimos detectarlo en la compilación. Ni siquiera podemos hacer que el IDE nos diga qué propiedades tiene este objeto. Así que seamos más exactos con los tipos:

export function createSafe<Target>(target: Target): Target { const handler = { get(target: Target, name: string | number | symbol, receiver: any) { if (hasKey(target, name)) { const targetElement = Reflect.get(target, name, receiver) if (isObject(targetElement)) { return createSafe(targetElement) } return targetElement } return notDefined }, } return new Proxy(target, handler) }

Aquí estamos haciendo uso de un genéricoSe abre en una nueva pestaña. Los genéricos nos permiten definir un tipo que puede tomar diferentes valores a lo largo del tiempo, que es lo que necesitamos para esta función. Dado que solo tenemos un parámetro, TypeScript sabe que el parámetro target obtendrá el valor del tipo Target:

const foo = createSafe('hello') // foo se infiere como un string const bar = createSafe(1) // bar se infiere como un número

Sin embargo, si compilamos veremos que obtenemos el siguiente error:

image-20191113201553918

¿Qué ha pasado? Bueno, dado que cualquier tipo puede asignarse a Target, eso plantea un problema, ya que solo queremos que Target tome cualquier tipo que sea de clase objeto. Así que lo que queremos aquí es restringir el tipoSe abre en una nueva pestaña que podemos asignar a Target al tipo object:

export function createSafe<Target extends object>(target: Target): Target { ... }

Con Target extends object estamos indicando que Target solo puede ser de tipo object.

Uff, César, esto parece muy complejo, ¿verdad? Pues sí, quería mostrar un ejemplo complejo por si tienes un problema similar. El resto del código era bastante sencillo, ¿verdad?

#Nos volvemos más estrictos

¿Y si te dijera que puedes hacer que tu programa sea a prueba de fuego? Bueno, puedes, aquí entra en juego el modo estrictoSe abre en una nueva pestaña:

{ "compilerOptions": { "strict": true } }

Con esta opción TypeScript ofrecerá más comprobaciones como nulos, this, bind, call, apply e inicialización de propiedades. También me gusta establecer las siguientes opciones:

{ "compilerOptions": { "strict": true, "noFallthroughCasesInSwitch": true, "noUnusedParameters": true, "noUnusedLocals": true, "forceConsistentCasingInFileNames": true } }

Ahora TypeScript me dirá cuando me olvidé de añadir una rama en una sentencia switch, cuando puse un parámetro en una función que no uso, cuando no uso un símbolo, cuando todas las ramas de una función devuelven un valor o cuando no estoy siendo consistente con las mayúsculas de las importaciones en diferentes archivos.

## Tests

Este proyecto tiene pruebas hechas con JestSe abre en una nueva pestaña. En cualquier refactorización es una buena idea tener una red de seguridad como una suite de pruebas. Si cometemos errores, las pruebas nos lo dirán. También tenemos a TypeScript para comprobar errores, pero si TypeScript tiene errores, no podremos compilar y desplegar a producción, ¿verdad? Bueno, no, TypeScript siempre compila a JavaScript, podríamos simplemente ignorar los errores. Aunque, ¿estás seguro de que quieres desplegar un programa con posibles errores?

Ejecutemos las pruebas:

npm test

image-20191113202705616

Oups, ¿qué está pasando? Bueno, JestSe abre en una nueva pestaña, el corredor de pruebas que estamos usando, no sabe cómo compilar TypeScript, lo cual requiere algo de configuración. Añadimos BabelSe abre en una nueva pestaña para que pueda eliminar los tipos y luego ejecutar las pruebas:

npm i @babel/preset-typescript -DE

Necesitamos crear un archivo babel.config.js en la raíz de nuestro proyecto:

module.exports = { presets: ['@babel/preset-typescript'], }

Añadimos algo de configuración a jest.config.js:

module.exports = { ... rootDir: 'src', moduleFileExtensions: ['ts', 'js'] }

¡Y ejecutamos las pruebas!

image-20191115194956149

#Conclusión

Si quieres ver todo el código que migré echa un vistazo a este pull requestSe abre en una nueva pestaña. Si todavía tienes algunas dudas puedes consultar este otro artículo que escribíSe abre en una nueva pestaña.

¿Y ahora qué? Bueno, verás las grandes ventajas que nos proporciona TypeScript como menos bugs, poder hacer una refactorización sencilla como renombrar un símbolo, o un programa que tiene una documentación viva. Esta y muchas otras ventajas solo por usar TypeScript.

Gracias por leer este post
¿Quieres apoyar ideas que merecen ser compartidas? Apoyar este blog me compra tiempo para escribir.
Más tiempo significa más valor para ti.