Introducción
Cuando desarrollamos en el lado de Front End, esta muy estandarizada la integración de TypeScript en nuestros proyectos, sea utilizando Babel + Webpack, o tirando de cli y plantillas.
Pero... ¿Y en una aplicación nodejs? ¿Cómo podemos añadir soporte a TypeScript? Aquí no solemos tirar de Webpack, ni generamos bundles, en este post veremos que pasos tenemos que dar.
TL;DR;
Para integrar nodejs con TypeScript una buena combinación es:
Tirar de tsc (TypeScript) para ejecutar la validación de tipos (ver que no hay errores).
Usar _Babel_ para transpilar los ficheros ts a js.
De Babel usaremos:
@babel/node: para ir trabajando en local, es un sustituto del cli de nodejs (haciendo una analogía con webpack, es como si fuera nuestro webpack-dev-server).
babel: para cuando queramos hacer un build tener el código transpilado.
Si quieres utilizar alias puedes usar babel-plugin-module-resolver
Si quieres poder debuggear tus ficheros TypeScript puedes usar Visual Studio Code.
Si te planteas minificar ficheros puedes usar: babel-preset-minify
Puedes encontrar un ejemplo en este enlace: https://github.com/Lemoncode/node-typescript-babel-example
Si tienes ganas de ver cómo funciona en detalle sigue leyendo :).
Agenda
Vamos a crear nuestro proyecto partiendo de cero, trabajando con un fichero js.
Vamos a migrar a ts y añadir el scaffolding para poder trabajar en local con Babel.
Vamos a aprender a usar alias.
Vamos a ver como debuggear con VS Code.
Finalmente vamos a ver cómo minificar la salida transpilada.
Manos a la obra
Punto de partida (JS)
Partimos de cero:
Creamos una carpeta, que podemos llamar _myexample_ (todo en minúsculas y sin espacios)
Abrimos el terminal y hacemos un _npm init_
npm init -y
Y vamos a crear un fichero _index.js_ debajo de la carpeta _src_.
./src/index.js
console.log("Hello node !");
En el package.json vamos a añadir un script para que arranque el proyecto:
_./package.json_
"scripts": { "start": "node ./src/index.js" }
Arrancamos el proyecto y vemos que aparece por consola el mensaje _Hello node !_
npm start
Ya tenemos nuestro proyecto mínimo tirando de _JavaScript_ es hora de añadir soporte a _TypeScript_
Migrando a TypeScript
Vamos a renombrar nuestro fichero index.js a index.ts y añadimos algo de código que necesite de un proceso de transpilación (en este caso un array numérico, y usamos interpolación de ES6 para crear un string):
./src/index.ts
const sales: number[] = [10, 40, 50]; const message = `Sales January ${sales[0]}`; console.log(message);
Instalando paquetes de babel
Nos hace falta enseñarle trucos nuevos a node: como manejarse con TypeScript tanto validando tipos como generando la transpilación de los ficheros de ts a js
Por un lado vamos a instalar de _babel_:
@babel/core: es decir su librería principal.
@babel/cli (Command line interface): para poder compilar ficheros desde la línea de comandos.
@babel/preset-env: Es decir una configuración estándar para poder transpilar ES6 a ES5.
@babel/preset-typescript: una configuración estándar para poder transpilar TypeScript.
@babel/node: es un cli que funciona exactamente igual que el cli de nodejs, pero nos va a permitir trabajar con TypeScript (él mismo se encarga de hacer la transpilación).
Desde la línea de comandos ejecutamos el siguiente comando:
npm install @babel/core @babel/cli @babel/preset-env @babel/preset-typescript @babel/node --save-dev
Trabajando en local
Ya tenemos todas las herramientas necesarias para trabajar, empezamos por poder arrancar el proyecto en nuestra máquina local.
Añadimos un fichero de configuración de babel
./.babelrc
{ "presets": ["@babel/preset-env", "@babel/preset-typescript"] }
Aquí cargamos los preset por defecto (un conjunto genérico), y también le indicamos que use los presets para TypeScript.
Vamos a cambiar el comando _start_ por el siguiente:
./package.json
"scripts": { "start": "babel-node --extensions \".ts\" src/index.ts" }
Lo que estamos haciendo es reemplazar el cli de node por babel-node y le decimos que tenga en cuenta la extensión de ficheros ts.
Arrancamos nuestra aplicación:
npm start
Y podemos comprobar que está funcionando y que ha realizado la transpilación (en memoria).
Muy importante: babel-node solo es aconsejable usarlo en el entorno local de desarrollo, para ir a producción lo que hacemos es desplegar el código ya transpilado (veremos como hacer eso en el siguiente apartado).
Hasta aquí parece que va todo sobre ruedas, pero... vamos a probar a introducir un fallo y ver que pasa (en la ultima entrada del array de tipo number[] vamos a introducir un string):
./src/index.ts
const sales: number[] = [10, 40, "bad entry"]; const message = `Sales January ${sales[0]}`;
Si ejecutamos un _npm start_ podemos ver que el proceso de build no nos da ningún fallo ¿Qué esta pasando aquí? Babel es muy rápido, y no se preocupa de ver si hay errores de tipado, para ello debemos de usar _tsc_ de TypeScript.
Vamos a instalarnos _TypeScript_ en nuestro proyecto:
npm install typescript --save-dev
Fíjate que Babel no hace uso de Typescript para transpilar los _ts_, tampoco hace chequeo de tipos.
Vamos a crear un fichero tsconfig.json para configurar nuestro proyecto TypeScript:
./tsconfig.json
{ "compilerOptions": { "target": "es6", "module": "es6", "moduleResolution": "node", "declaration": false, "noImplicitAny": false, "sourceMap": true, "noLib": false, "allowJs": true, "suppressImplicitAnyIndexErrors": true, "skipLibCheck": true, "esModuleInterop": true, "baseUrl": "./src" }, "include": ["src/**/*"] }
Ya parece que lo tenemos todo listo para poder lanzar nuestro proceso, una primera opción podría ser modificar el comando start, y hacer lo siguiente:
Primero ejecutar _tsc_ para hacer el chequeo de tipos.
Segundo, si todo ha ido bien ejecutar _babel-node_ para generar el código transpilado.
Utilizando && podemos encadenar dos comandos:
./package.json
"scripts": { "start": "tsc --noEmit && babel-node --extensions \".ts\" src/index.ts" }
Nos queda el siguiente flujo:
Si ahora ejecutamos podemos verlo funcionar:
npm start
Pero esto tiene una pega y es que la transpilación de _babel_ no arranca hasta que no ha terminado el chequeo de tipos (se ejecutan de manera secuencial) ¿No podríamos ejecutar estos dos procesos en paralelo? La respuesta es si, vamos a instalar un paquete llamado npm-run-all que nos permitirán ejecutar ambas tareas a la vez.
npm install npm-run-all --save-dev
En el package.json, crearemos un comando de script para el chequeo de tipos y otro para la ejecución de babel:
"scripts": { "type-check": "tsc --noEmit", "start:dev": "babel-node --extensions \".ts\" src/index.ts" }
Y ahora vamos a hacer uso del npm-run-all en el comando start, aquí lanzamos en paralelo los dos scripts anteriores, verás que usamos un parámetro "-l" esto sirve para que la salida por consola de cada tarea se pinte con colores distintos (de esta manera es más fácil de distinguir que mensajes son de chequeo de tipos y cuales son del transpilador).
"scripts": { "start": "run-p -l type-check start:dev", "type-check": "tsc --noEmit", "start:dev": "babel-node --extensions \".ts\" src/index.ts" }
Con lo que nos queda el siguiente flujo de ejecución:
Podemos ejecutar y ver que ahora ambas tareas corren en paralelo:
npm start
Esto está muy bien, pero a mi me gustaría que si modifico un fichero en el editor, automáticamente se volviera a lanzar el build y se reflejaran los cambios en la aplicación que estoy ejecutando en local ¿Se puede? Si, esto lo vamos a hacer de la siguiente manera:
TypeScript tsc ofrece un parámetro (_--watch_) en el que se queda esperando y si detecta algún cambio en los ficheros TS vuelve a lanzar el chequeo de tipos.
En babel-node lo que hacemos es tirar de _nodemon_, esta herramienta detecta si hay cambios en el directorio y reinicia la aplicación _node_ que este monitorizando (_babel-node_ en este caso).
Instalamos el paquete nodemon (reinicia una aplicación cuando detecta cambios en la carpeta de trabajo)
npm install nodemon --save-dev
Realizamos las modificaciones en el package.json en la sección de scripts (añadimos la entrada type-check:watch añadiendo el flag —watch y en start:dev invocamos a babe-node desde nodemon):
./package.json
"scripts": { "start": "run-p -l type-check:watch start:dev", "type-check:watch": "npm run type-check -- --watch", "type-check": "tsc --noEmit", "start:dev": "nodemon --exec babel-node --extensions \".ts\" src/index.ts" }
Si ejecutamos desde el terminal, podemos ver que si cambiamos el código de nuestro proyecto, automáticamente se dispara la transpilación y el chequeo de tipos.
npm start
Un resumen de como ha quedado esto:
La sección scripts que se nos ha quedado en el package.json es la siguiente:
./package.json
"scripts": { "start": "run-p -l type-check:watch start:dev", "type-check:watch": "npm run type-check -- --watch", "type-check": "tsc --noEmit", "start:dev": "nodemon --exec babel-node --extensions \".ts\" src/index.ts" },
Generando un build
Esto está muy bien para desarrollar en local ¿Y si queremos desplegar esto en producción? ¿Tengo que usar babel-node? La respuesta es no (la propia página oficial te lo desaconseja), lo que haces en ese caso es que lanzas el proceso de build y lo vuelcas a un directorio (por ejemplo a la carpeta dist), lo que se genera en esa carpeta ya es un código transpilado que te puedes llevar directamente a producción.
Que pasos vamos a seguir:
Para volcar contenido en la carpeta dist, primero nos aseguramos de que esté vacía: vamos a borrar el contenido que pudiera tener. Como el comando de shell para borrar de carpetas no es el mismo en Linux/Windows, vamos a utilizar una librería que si es multiplataforma: rimraf
Vamos a generar el build en la carpeta dist usando babel.
En paralelo haremos un chequeo de tipos como hicimos en el paso anterior.
Primero instalamos rimraf (borrado de carpetas), ejecutamos el siguiente comando desde el terminal:
npm install rimraf --save-dev
Y vamos a crear un comando en la sección de scripts para lanzar el borrado de la carpeta dist (ver ultima línea, comando clean)
./package.json
"scripts": { "start": "run-p -l type-check:watch start:dev", "type-check:watch": "npm run type-check -- --watch", "type-check": "tsc --noEmit", "start:dev": "nodemon babel-node --extensions \".ts\" src/index.ts", "clean": "rimraf dist" }
Añadimos un comando para hacer un build a producción (ver última línea _build:prod_):
"scripts": { "start": "run-p -l type-check:watch start:dev", "type-check:watch": "npm run type-check -- --watch", "type-check": "tsc --noEmit", "start:dev": "nodemon babel-node --extensions \".ts\" src/index.ts", "clean": "rimraf dist", "build:prod": "npm run clean && babel src -d dist --ignore=\"./src/**/*.spec.ts\" --extensions \".ts\"" }
¿Qué estamos haciendo en este comando build:prod?
Primero borramos la carpeta _dist_, esta vez lo ejecutamos de manera secuencial: eliminamos la carpeta y vamos a por el siguiente comando.
Después lanzamos la transpilación de los ficheros, le indicamos que el resultado lo vuelque a la carpeta _dist_ y también le indicamos que ignore ficheros de tests y que trabaje con la extensión de Typescript.
Por último vamos a lanzar en paralelo el proceso de chequeo de tipos con el de build (ver última línea):
_./package.json_
"scripts": { "start": "run-p -l type-check:watch start:dev", "type-check:watch": "npm run type-check -- --watch", "type-check": "tsc --noEmit", "start:dev": "nodemon babel-node --extensions \".ts\" src/index.ts", "clean": "rimraf dist", "build:prod": "npm run clean && babel src -d dist --ignore=\"./src/**/*.spec.ts\" --extensions \".ts\"", "build": "run-p -l type-check build:prod" }
El flujo que nos queda:
También podríamos haberlo ejecutado todo en secuencial, como ejercicio ¿Te animas a implementarlo? ¿ Qué ventajas podría tener ejecutarlo todo en secuencial?
Si lanzamos el comando npm run build podemos ver como se genera en la carpeta dist el código transpilado:
npm run build
Bonus
Bonus Aliases
Para terminar vamos a ver cómo configurar alias con Babel.
¿Esto que es? Es algo así como atajos para hacer imports de carpetas que están en el raíz.
Veamos un ejemplo: nos vamos a crear una utilidad para sumar todos los elementos del array, lo vamos a colocar dentro de la carpeta common.
./src/common/index.ts
export const sumTotal = (elements: number[]): number => elements.reduce((accumulator, element) => accumulator + element, 0);
Si ahora queremos hacer uso de esto en nuestro index.ts, lo importamos y lo llamamos (reemplazar todo el index.ts con este snippet):
./src/index.ts
import { sumTotal } from "./common"; const sales: number[] = [10, 40, 30]; const total = sumTotal(sales); const message = `Sales January ${total}`; console.log(message);
Aquí no se nota mucho el impacto de usar rutas relativas, pero imagínate que ese fichero estuviera dentro de dos subcarpetas, el import que podemos ver en la primera línea quedaría tal como import {sumTotal} from '../../common', ¿No sería más cómodo tener un import tal como...?
import { sumTotal } from "common";
¿Y que diera igual en que subcarpeta estas?
Esto es fácil de hacer con Webpack y Typescript, pero…¿Podemos hacerlo directamente con Babel y TypeScript? Veamos como:
En este caso en TypeScript con esta línea en el tsconfig.json (ya está en el ejemplo) le decimos que tenga en cuenta las carpetas que hay bajo src
./tsconfig.json
"include": ["src/**/*"]
Y para Babel, instalamos el plugin babel-plugin-module-resolver (nos permite definir alias para directorios y ficheros):
npm install babel-plugin-module-resolver --save-dev
Y lo configuramos en el fichero .babelrc (ver sección de plugins):
./.babelrc
{ "presets": ["@babel/preset-env", "@babel/preset-typescript"], "plugins": [ [ "module-resolver", { "root": ["."], "alias": { "common": "./src/common" } } ] ] }
Ahora, estemos en la carpeta que estemos, podemos cambiar nuestro import por el siguiente:
import { sumTotal } from "common";
Bonus depuración
Vamos a ver como depurar (debuggear) nuestra aplicación TypeScript corriendo directamente desde nuestro Visual Studio Code.
Añadimos una nueva entrada a la sección de scripts para generar un fichero de build con maps (estos ficheros hacen un mapeo entre código fuente transpilado y código TypeScript), la llamaremos build:dev (penúltima línea el snippet, flag --source-maps).
./package.json
"scripts": { "start": "run-p -l type-check:watch start:dev", "type-check:watch": "npm run type-check -- --watch", "type-check": "tsc --noEmit", "start:dev": "nodemon --exec babel-node --extensions \".ts\" src/index.ts", "clean": "rimraf dist", "build:prod": "npm run clean && babel src -d dist --ignore=\"./src/**/*.spec.ts\" --extensions \".ts\"", "build:dev": "npm run clean && babel src -d dist --ignore=\"./src/**/*.spec.ts\" --extensions \".ts\" --source-maps", "build": "run-p -l type-check build:prod" },
Nos vamos a la pestaña de _debug_ en el sidebar de VSCode:
Elegimos la opción create a launch.json file
Elegimos nodejs (preview)
Esto nos genera una configuración por defecto, que necesita que la personalicemos:
Por un lado tenemos que indicarle que el punto de entrada para depurar está en dist/index.js (es decir el resultado transpilado de hacer un build).
Por otro lado indicarle que antes de arrancar la depuración ejecute el comando de build:dev que hemos definido anteriormente para asegurarnos que tenemos en dist un código transpilado con sus ficheros map listos para depurar.
Vamos a crear debajo de la carpeta _.vscode_ el siguiente fichero:
./vscode/tasks.json
{ "version": "2.0.0", "command": "npm", "type": "shell", "tasks": [ { "label": "build", "args": ["run", "build:dev"], "group": "build" } ] }
Lo que hacemos en este fichero es definir una tarea build que ejecutará el comando npm run build:dev (de esta manera VSCode sabrá como ejecutarlo).
Volvemos al fichero launch.json generado y añadimos este comando en la sección preLaunchTask y también nos aseguramos que el punto de entrada en la sección "program" apunta a _${workspaceFolder}/dist/index.js_
Nos tendría que quedar algo tal que así
./vscode/launch.json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"preLaunchTask": "build", // Execute build task
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/index.js", // Indicate entry point
"outFiles": [
"$/**/*.js"
]
}
]
}
Ahora podemos poner un break point en nuestro código:
Le damos al botón de play para arrancar la depuración:
Y... ¡listo! Ya podemos depurar nuestra aplicación _nodejs_ usando nuestro editor favorito :).
Bonus Minify
Normalmente en una aplicación nodejs, al correr en servidor no nos planteamos minificar los archivos JS, ya que estos no van a salir del servidor, pero... si trabajas con serverless o lambdas si podría interesarte mantener pequeño el tamaño de tus fichero generados. Vamos a ver que tal se porta babel-minify.
Vamos a instalar el preset:
npm install babel-preset-minify --save-dev
Configuramos el preset (primera línea, array de presets):
./babelrc
{ "presets": ["@babel/preset-env", "@babel/preset-typescript", "minify"], "plugins": [ [ "module-resolver", { "root": ["."], "alias": { "common": "./src/common" } } ] ] }
Si ahora generamos un build podemos ver como los ficheros de salida están minificado:
npm run build
Recursos
Código de este post: https://github.com/Lemoncode/node-typescript-babel-example
Otro ejemplo de proyecto semilla, Express + TypeScript scaffolding: https://github.com/Lemoncode/scaffolding-express-typescript
¿Front, Devops o Back?
Si tienes ganas de ponerte al día Front, Devops o Backend ¿Te apuntas a alguno de nuestros Masters Online o Bootcamps?