¡Hola! Me parece que andas usando un bloqueador de anuncios =( ¿Nos desbloqueas? ¿Por qué?
F13

Funcion 13

Aprende TypeScript

En mi camino hacia poder enseñar sobre Angular2, TypeScript es sin duda una parada importante. Por si no te suena de nada, TypeScript es un lenguaje que se compila en JavaScript creado por Microsoft. Tiene muchos conceptos de lenguajes como C# o Java y, en cierto modo, le quita toda la libertad del tipado libre a JavaScript convirtiéndolo en algo mucho más estricto, ganando claridad en el código en el proceso.

A mi, personalmente, no me hacía mucha gracia TypeScript mientras que era un usuario acérrimo (por motivos profesionales inicialmente) de Closure Compiler y de JSDoc, por lo que me di cuenta de que al fin y al cabo era lo mismo, solo que no cumplir con los tipos, tenía consecuencias.

¿Por qué usar TypeScript?

Antes de meternos en faena, es posible que te preguntes. ¿Para qué quiero aprender algo que al final se convierte en JavaScript? JavaScript ya es bueno tal cual por lo que la duda es totalmente válida. No hace falta aprender TypeScript y hay mucha gente que no lo usa para nada. No obstante, TypeScript tiene algunos beneficios que quizá quieras valorar.

  • TypeScript hace que sea sencillo el organizar código para aplicaciones grandes gracias a los módulos, espacios de nombres y la orientación a objetos. Estos conceptos eran prácticamente ajenos a JavaScript.
  • Gracias al tipado, el código que genera TypeScript es fácil de leer, predecible y más sencillo de depurar. Además en el proceso de compilación, TypeScript será capaz de encontrar errores antes de que el código se ejecute.
  • Los IDEs como WebStorm, Visual Studio o editores más sencillos como Atom o Sublime, son capaces de ayudarte a escribir el código ya que conocen de antemano con el tipo de variable/objeto/clase que estás tratando.
  • Nos permite usar características aun no implementadas por los navegadores hoy mismo. Expresiones como async o await aun no están soportadas por todos los navegadores pero TypeScript nos permite usarlos hoy, y obtener un código funcional en todos los navegadores.
  • Angular 2 está escrito en TypeScript y a día de hoy es necesario usarlo para usar el framework.

El tema de Angular 2 tiene bastante controversia, al menos para mi. Los tutoriales de Angular 2 prometen dos versiones: ES6 y TypeScript. Pero en la realidad, solo el de TypeScript está completo y según mis fuentes, no se van a dar mucha prisa en completar los otros. La comunidad ha aceptado este hecho y poco a poco todo el mundo está usando TypeScript con Angular 2 y la gran mayoría de los tutoriales usan este pseudo-lenguaje.

¿Aun no estás convencido? Te recomiendo que leas este fantástico artículo de Micael Gallego: "Como soy torpe, me gusta TypeScript".

¿Te pica la curiosidad? Pues adelante con la lectura. Este tutorial está pensado para personas que tengan bastantes conocimientos de JavaScript pero no tengan ni idea de TypeScript. Es necesario tener instalado Node.js.

Nota: Es posible que todo esto de Node.js te suene pero si quieres saber más y aprender, te recomiendo que leas mi libro Descubriendo Node.js y Express.

Instalando TypeScript

El compilador de TypeScript puede ser instalado como paquete de Node y para ello necesitamos usar npm por lo que escribiremos:

$ npm install -g typescript

Esto descargará e instalará el compilador. Una vez terminado, puedes probar que funciona escribiendo en tu terminal tsc -v para revisar la versión y que todo funcione correctamente:

$ tsc -v
Version 2.1.4  

Compilando a JavaScript

Cuando escribimos código en TypeScript generalmente lo haremos en ficheros con extensión .ts (en vez de .js). El navegador no es capaz de entender estos ficheros y necesita traducirlos primero a JavaScript. La compilación se puede realizar de varias formas:

  • En el terminal, usando tsc.
  • Usando un gestor de tareas de JavaScript como Grunt o Gulp.
  • A través de algunos IDEs como WebStorm o Visual Studio.

En el caso que nos ocupa, escogeremos la primera opción ya que es la opción más sencilla y no necesita más configuración pero es probable que acabes usando Grunt/Gulp/Webpack en tu proyecto en vez de hacerlo a mano.

Para convertir un fichero TypeScript a su homólogo en JavaScript tendremos que escribir lo siguiente:

$ tsc fichero.ts

Esto cogerá el archivo con nombre fichero.ts y lo convertirá en fichero.js. Obviamente el nombre lo escoges tú.

Es posible también compilar varios archivos de una vez, o incluso usar un asterisco para no tener que especificar varios nombres:

# Generará app.js y usuario.js
$ tsc app.ts usuario.ts

# Compila todos los ficheros .ts del directorio actual a JavaScript
$ tsc *.ts

Es posible que no quieras llamar al fichero de salida, de la misma forma que el fichero de entrada. Para ello, escribimos --outFile y usamos el nombre que queramos:

# Generará aplicacion.js en vez de app.js
$ tsc app.ts --outFile aplicacion.js

Otra opción bastante interesante es la de vigilar cambios en un fichero con -w o --watch de manera que cuando se detecte algún cambio, TypeScript directamente compile el fichero sin tener nosotros que intervenir. ¡Muy práctico!

Configurando TypeScript con tsconfig.json

Todo lo que hemos hecho antes estaba realmente bien aunque habitualmente usaremos una serie de opciones para todos nuestros proyectos para lo que podemos crear un fichero tsconfig.json con las opciones que necesitemos. Vamos a ver algunas de las opciones más interesantes y prácticas para configurar el compilador.

Podemos hacerlo manualmente pero TypeScript nos ofrece un comando para hacerlo:

tsc --init  

Si lo hemos hecho bien, deberíamos tener un fichero tsconfig.json en la raíz de nuestro proyecto. Veamos qué contiene:

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "noImplicitAny": false,
        "sourceMap": false
    }
}

Estas son las opciones por defecto que nos genera el compilador. No vamos a entrar en ellas por ahora pero la ventaja es que ya no necesitamos escribir el nombre de los ficheros a compilar. Invocando tsc, conseguiremos que todos los ficheros con extensión .ts sean compilados a JavaScript. Por defecto (aunque no aparezca) excluye el directorio node_modules que, como ya sabrás, es donde se instalan las dependencias de npm de nuestro proyecto.

Nota: Todas estas opciones que ves en compilerOptions las puedes pasar de forma manual con el comando tsc. Por ejemplo, tsc --target es6.

Excluyendo directorios y archivos

Si queremos excluir otros directorios y/o ficheros, podemos añadir exclude de la siguiente forma:

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "noImplicitAny": false,
        "sourceMap": false
    },
    "exclude": [
        "tests/**/**.ts"
    ]
}

En este caso, con "tests/**/**.ts" estamos diciendo que se excluyan todos los ficheros con extensión .ts que estén dentro del directorio tests.

Cambia el directorio de salida

Es muy común el tener los archivos fuentes en un sitio, y los archivos compilados en otro. Para ello necesitamos la opción outDir del compilador:

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "noImplicitAny": false,
        "sourceMap": false,
        "outDir": "./build"
    }
}

Esto hará que todos los ficheros con la extensión .ts acaben en el directorio build, manteniendo la misma estructura.

Evitar compilación en errores

TypeScript compilará los archivos incluso si hay errores (aunque no dejará de mostrarlos). No obstante, este comportamiento no es siempre deseable y para ello tenemos la opción noEmitOnError cuyo valor por defecto es false. Si cambiamos su valor a true, TypeScript no compilará los ficheros a JavaScipt.

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "noImplicitAny": false,
        "sourceMap": false,
        "noEmitOnError": true
    }
}

Hay muchas más opciones para usar en este fichero pero se escapan un poco del alcance de este tutorial. Si sientes curiosidad, te recomiendo que le eches un vistazo a la documentación oficial (en inglés).

Tipado estático

Uno de los usos más comunes de TypeScript es el tipado estático. Esto implica que se pueden declarar los tipos de las variables y el compilador se asegurará de que no se usan tipos de datos erróneos.

Vamos a ver un ejemplo:

var pagina: string = 'funcion'; // Cadena  
var numero: number = 13; // Número  

Si se omite el tipo, TypeScript intentará detectar el tipo por el código.

var pagina = 'funcion'; // Cadena  
var numero = 13; // Número  

Los tipos también podemos incluirlos en los argumentos a funciones:

function abrir(pagina: string, numero: number) {  
  console.log(`Voy a visitar ${pagina}${numero}`);
}

abrir('funcion', 13);  
// Voy a visitar funcion13

En este momento puede que te estés preguntando, ¿cómo sabe TypeScript lo que voy a pasar? Si está revisando los tipos continuamente, seguramente sea muy costoso en términos de recursos. Te llevarías la sorpresa al abrir el archivo compilado y ver que los tipos desaparecen directamente:

function abrir(pagina, numero) {  
    console.log(`Voy a visitar ${pagina}${numero}`);
}
abrir('funcion', 13);  
// Voy a visitar funcion13 

No obstante, si cambio el segundo parámetro de 13 a "13" (una cadena). TypeScript me dará un error:

$ tsc app.ts
app.ts(5,18): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.  

Lo mismo ocurre si no indicamos tipos e intentamos reconvertir una variable a otro tipo:

var pagina = 'funcion';

pagina = 13;  

Nos dirá:

$tsc app.ts
app.ts(3,1): error TS2322: Type 'number' is not assignable to type 'string'.  

Estos son los tipos más comunes:

  • number - Todos los tipos de valores numéricos. Enteros, Flotantes, Hexadecimal, Octal. Todos valen.
  • string - Cadenas de caracteres, tenemos las ' comillas simples, " comillas dobles y las ``` tildes para las plantillas de ES6.
  • boolean - true o false.
  • any - Este tipo permite cualquier tipo de datos en cualquier momento ya que el valor puede venir de un contenido dinámico o de una librería. Es muy útil para comenzar poco a poco con TypeScript ya que el compilador ignorará esta variable.
  • void - Este es el opuesto a any y es básicamente la ausencia de valor. Se usa para definir que una función no devuelve nada. Si lo usas en una variable no tendrá mucho sentido ya que solo podrá ser null o undefined.
  • Array - Para las matrices. Tiene dos sintaxis posibles matriz: string[] o matriz: Array<string>. De esta forma indicamos que la variable es una matriz que contiene cadenas de caracteres.

Interfaces

Las interfaces se usan para revisar si un objeto o clase cumplen con una cierta estructura. Cuando definimos una interfaz, establecemos un contrato en el que decimos que cualquier clase o variable que implementen la interfaz, tienen que tener esas variables y/o métodos.

Veamos esto con un ejemplo:

// Interfaz Usuario, con sus propiedades nombre,
// correo y edad
interface Usuario {  
  nombre: string;
  correo: string;
  edad: number;
}

// La función espera un objeto que tenga las propiedades
// nombre, correo y edad
function saludar(usuario: Usuario): void {  
  console.log(`Hola ${usuario.nombre}`);
}

// El objeto que pasamos a la función, cumple con
// la interfaz Usuario
saludar({  
  nombre: 'Antonio',
  correo: 'a.laguna@funcion13.com',
  edad: 29
});

El orden de las propiedades, por si te lo estás preguntando, no es importante. Lo único importante es que estén presentes y que el tipo sea el indicado. En el momento en que falte algo o el tipo no sea correcto, el compilador nos avisará.

Propiedades opcionales

No siempre vamos a necesitar todas las propiedades de una interfaz. Puede que algunas existan o puede que no. Un caso de uso muy común es el caso de recibir un objeto de opciones que contenga solo algunas propiedades. Por ejemplo:

interface Animacion {  
  propiedad: string;
  duracion?: number;
}

function animar(animacion: Animacion) {  
  let { duracion = 300, propiedad } = animacion;
  console.log(`Animando ${propiedad} durante ${duracion}ms`);
}

animar({  
  propiedad: 'width',
  duracion: 500
});

animar({  
  propiedad: 'opacity'
});

En este caso no obtendríamos error alguno ya que duracion es un totalmente opcional.

Clases

En las clases nos detendremos menos ya que las clases están presentes de forma nativa en ES6. Es un concepto que a muchos programadores les gusta, especialmente viniendo de otros lenguajes como Java.

TypeScript añade más a las clases de lo que añade ES6: Abstracción, implementación de interfaces, el concepto de público, privado y protegido.

Veamos una sencilla clase de TypeScript:

class Persona {  
  nombre: string;

  constructor(nombre: string) {
    this.nombre = nombre;
  }

  saludar(): void {
    console.log(`Hola, me llamo ${this.nombre}`);
  }
}

Hasta aquí espero que nada raro. Tenemos una clase Persona con una propiedad nombre que es una cadena. Y tenemos un método saludar (que no devuelve nada ya que tiene void) y que muestra en la consola nuestro nombre. Hasta aquí, salvo los tipos, todo es igual. Podemos instanciar la clase usando new, lo cual invocaría el constructor:

let yo = new Persona('Antonio');

yo.saludar();  
// Hola, me llamo Antonio

Herencia

La herencia funciona igual que en JavaScript. Veamos un breve ejemplo:

class Trabajador extends Persona {  
  empresa: string;

  constructor(nombre: string, empresa: string) {
    super(nombre);
    this.empresa = empresa;
  }

  saludar(): void {
    super.saludar();
    console.log(`Estoy trabajando en ${this.empresa}`);
  }
}

let alguien = new Trabajador('Alberto', 'Facebook');

alguien.saludar();  
// Hola, me llamo Alberto
// Estoy trabajando en Facebook

Hasta ahora todo es exactamente igual que en ES6.

Podemos usar extends para crear una clase que herede de otra. Las clases heredadas tienen que llamar a super en el constructor (con los parámetros que éste necesite) para que se ejecute el constructor de la clase base.

También podemos sobrescribir otros métodos, en este caso saludar, a la vez que llamar al método de la clase base usando super.

Pasemos a cosas que no nos ofrece ES6.

public, private y protected

En los ejemplos que he escrito antes, somos capaces de acceder métodos y propiedades sin problema ninguno, Esto es porque son consideradas como públicas. Podríamos haber puesto la palabra public al lado pero no es necesario. Esto es también válido:

class Persona {  
  public nombre: string;

  public constructor(nombre: string) {
    this.nombre = nombre;
  }

  public saludar(): void {
    console.log(`Hola, me llamo ${this.nombre}`);
  }
}

Pasemos a private. Cuando un método o propiedad es marcado como private, se considera privado. Esto significa que no podemos acceder a ellos desde fuera de la clase. Ni siquiera en clases heredadas.

class Persona {  
  private nombre: string;

  constructor(nombre: string) {
    this.nombre = nombre;
  }
}

let persona = new Persona('Antonio');  
persona.nombre = 'Federico'; // Error: 'nombre' es privado  

Veamos protected ahora. Este modificador actúa de manera similar a private con la excepción de que se puede acceder en clases heredadas:

class Persona {  
  protected nombre: string;

  constructor(nombre: string) {
    this.nombre = nombre;
  }
}

class Trabajador extends Persona {  
  private empresa: string;

  constructor(nombre: string, empresa: string) {
    super(nombre);
    this.empresa = empresa;
  }

  saludar(): void {
    console.log(`Soy ${this.nombre}, y estoy trabajando en ${this.empresa}`);
  }
}

let trabajador = new Trabajador('Mark', 'Facebook');  
trabajador.saludar(); // Soy Mark, y estoy trabajando en Facebook

console.log(trabajador.empresa); // error  
console.log(trabajador.nombre); // error  

Como ves, la clase Trabajador puede acceder a la propiedad nombre ya que es protected.

TypeScript nos permite un atajo realmente útil para declarar propiedades que recibe una clase en el constructor y para ello necesitamos usar los modificadores de acceso. Veamos un breve ejemplo de nuestra clase sin este atajo:

class Persona {  
  protected nombre: string;

  constructor(nombre: string) {
    this.nombre = nombre;
  }
}

Y ahora con atajo:

class Persona {  
  constructor(
    protected nombre: string
  ) {
  }
}

Como ves, no necesitamos definirla como propiedad de la clase ni indicar this.nombre = nombre;. ¡Práctico!

Implementando Interfaces

Las clases pueden implementar una, o varias interfaces para forzar a la implementación de ciertos métodos. Veamos un breve ejemplo:

interface Asalariado {  
  cobrar(): void;
}

class Trabajador extends Persona implements Asalariado {  
  private empresa: string;

  constructor(nombre: string, empresa: string) {
    super(nombre);
    this.empresa = empresa;
  }

  saludar(): void {
    console.log(`Soy ${this.nombre}, y estoy trabajando en ${this.empresa}`);
  }
}

Si intentamos compilar esto, recibiremos un error:

app.ts(13,7): error TS2420: Class 'Trabajador' incorrectly implements interface 'Asalariado'.  
  Property 'cobrar' is missing in type 'Trabajador'.

Como ves, estamos intentando implementar la interfaz Asalariado en la clase Trabajador pero como no hemos definido el método cobrar, TypeScript nos dará un error.

Módulos

Un concepto importante al que ya estarás acostumbrado si usas Node, es el hecho de poder separar los ficheros en módulos. Si tenemos que tener todas las clases e interfaces en un mismo fichero para poder usar herencias e implementaciones, las cosas se pondrían feas muy pronto.

Es por ello que TypeScript nos permite exportar e importar código. Es cierto que TypeScript tiene varias formas de realizar esto dependiendo de si queremos un entorno AMD o uno igual al de ES6. Este último es el que vamos a mostrar:

Veamos nuestro archivo interfaces.ts:

export interface Asalariado {  
  salario: string;
  cobrar(): void;
}

export interface Empleado {  
  departamento: string;
}

Como ves, hemos añadido la palabra clave export para poder exportar dichas interfaces.

Y ahora veamos nuestra clase Trabajador en el fichero trabajador.ts.

import {Asalariado, Empleado} from "./interfaces";

class Trabajador implements Asalariado, Empleado {  
  salario: number;
  departamento: string;

  cobrar(): void {
    console.log(`He ganado ${this.salario}`);
  }
}

En este caso, estamos importando ambas interfaces desde el archivo interfaces con el comando import. También tenemos la opción de importarlos todos, bajo un espacio de nombre común:

import * as Interfaces from "./interfaces";

class Trabajador implements Interfaces.Asalariado, Interfaces.Empleado {  
  salario: number;
  departamento: string;

  cobrar(): void {
    console.log(`He ganado ${this.salario}`);
  }
}

Como ves, estamos diciendo:

Importa todo () *como Interfaces desde ./interfaces

El nombre Interfaces actúa como alias y podríamos haberlo cambiado por cualquier otra palabra como import * as MisCosas from "./interfaces"; solo que en vez de Interfaces.Asalariado tendríamos que escribir MisCosas.Asalariado.

Nada complejo, ¿no?

Genéricos

Los genéricos son una característica de TypeScript que suele costar entender aunque es una característica bastante potente.

Podríamos definir un Genérico como una plantilla que permite que una función/clase/variable acepte distintos tipos de valores pero sin usar any.

Vamos a imaginar que tenemos una clase que queremos poder usar para almacenar elementos de un tipo y poder concatenarlos (si son cadenas) o sumarlos (si son números). Intrínsecamente, la lógica es exactamente la misma para cadenas que para números. ¿Pero cómo hacemos con los tipos?

Escribamos la clase sin ellos.

class Lista {  
  elementos;

  constructor() {
    this.elementos = [];
  }

  anadir(elemento) {
    this.elementos.push(elemento);
  }

  sumar() {
    let resultado;

    this.elementos.forEach(a => {
      if (!resultado) {
        resultado = a;
      } else {
        resultado += a;
      }
    });

    return resultado;
  }
}

Hasta aquí nada nuevo. Si la instanciamos y la usamos:

let misNumeros = new Lista();  
misNumeros.anadir(10);  
misNumeros.anadir(3);  
console.log(misNumeros.sumar());  
// 13

let misCadenas = new Lista();  
misCadenas.anadir('funcion');  
misCadenas.anadir('13');  
console.log(misCadenas.sumar());  
// "funcion13"

¿Qué pasa si hacemos esto por error?

let misNumeros = new Lista();  
misNumeros.anadir(10);  
misNumeros.anadir('funcion');  
misNumeros.anadir(3);  
console.log(misNumeros.sumar());  
// "10funcion3"

¡Oops! Vamos a ver cómo podríamos arreglar esto.

class Lista<T> {  
  elementos: Array<T>;

  constructor() {
    this.elementos = [];
  }

  anadir(elemento: T) {
    this.elementos.push(elemento);
  }

  sumar(): T {
    let resultado;

    this.elementos.forEach(a => {
      if (!resultado) {
        resultado = a;
      } else {
        resultado += a;
      }
    });

    return resultado;
  }
}

Bueno, esto ya pinta mejor. ¿Qué es T? T es la convención para un genérico. En realidad podríamos haberlo llamado CualquierCosa y hubiera funcionado igual. Lo importante es que en todos los sitios en los que usemos ese genérico, sea el mismo. Podríamos haber declarado varios si quisiéramos class Lista<T, V, K> { ... }.

Hemos añadido T en vario sitios:

  • La definición de la clase. Ahí es donde declaramos todos los genéricos de una clase concreta.
  • En el método anadir. Todos los elementos que vamos a añadir son o number o string pero nunca mezclados.
  • En el método sumar. Lo usamos para indicar que lo que va a devolver es T, es decir o un number o string.

Veamos cómo podemos usar esto:

let misNumeros = new Lista<number>();  
misNumeros.anadir(10);  
misNumeros.anadir('funcion');  
misNumeros.anadir(3);  
console.log(misNumeros.sumar());  

Como ves, hemos creado una instancia de Lista, indicando entre signos de mayor/menor que, el tipo que T va a ser. En este caso number. Ahora, si intentamos compilar este código, obtendremos esto:

$ Argument of type 'string' is not assignable to parameter of type 'number'.

¡Perfecto! Como ves el genérico nos ha ayudado a cazar un error antes de que pueda llegar a ningún sitio.

Conclusión

Aunque TypeScript pueda no ser necesario, los beneficios de su uso son realmente bienvenidos. Es altamente valorado por desarrolladores que vengan de lenguajes con un tipado más fuerte y también es útil para los más avezados en JavaScript ya sea para reducir el número de errores como para obtener ayuda del editor de código a la hora de escribirlo.

Este tutorial está pensado como una introducción a TypeScript y, aunque contiene algunos temas un poco más avanzados, TypeScript tiene mucho más que ofrecer.

Hay otros temas de los que pienso escribir y que añadiré un enlace aquí.

¿Hay algo que no haya quedado claro? ¿Se te atasca alguna sección? ¿Echas en falta algo en particular? ¡Házmelo saber en los comentarios!

Programador Front-end en First + Third y Potato. Trabajando con JavaScript y HTML5 desde el corazón de Sevilla.

Comentarios ¡Únete a la conversación!