Objetivos

  • Al finalizar la unidad el alumno tendrá los conocimientos necesarios para escribir sencillas aplicaciones en typescript, compilar el código typescript a javascript y ejecutarlo en un entorno de ejecución.
  • También sabrá las ventajas que ofrece la anotación de tipos a la hora de prevenir errores en la escritura del código y como ayuda a mejorar la mantenibilidad del mismo.

Videos de la unidad

Esta unidad no tiene videos.

Por qué Typescript

De las muchas clasificaciones que existen para encuadrar a los lenguajes de programación, una de las más conocidas es la que atiende a la manera en que declaramos las variables.

A los lenguajes que exigen declarar explícitamente el tipo de variables se les denomina estáticamente tipados, mientras que a los que dejan en manos del intérprete/compilador la tarea de reconocer el tipo en función del valor que se le asigne se denominan dinámicamente tipados.

Javascript es dinámicamente tipado. Esto suele considerarse una ventaja pues ahorra algo de código al no tener que escribir el tipo de las variables, reduce la atención que hemos de poner cuando declaramos variables y da lugar a un código con menos cosas que interpretar para el programador.

Sin embargo las ventajas que hemos indicado en el párrafo anterior se pueden convertir rápidamente en desventajas. No prestar la atención debida en la declaración de las variables puede dar lugar a que seamos algo descuidados y cometamos más fallos. Y el problema es que estos fallos no los detecta el compilador/intérprete y pasan a convertirse en errores en tiempo de ejecución, que suelen ser más difíciles de encontrar y corregir.

Exigir que se declare el tipo de una variable implica que el compilador/intérprete puede detectar la asignación a una variable de un valor con tipo erróneo con la misma facilidad que detecta un error de sintaxis. De manera que esos errores saltan antes de que el programa llegue a ejecutarse. El propio compilador/intérprete nos indicará el lugar exacto donde se ha cometido.

¿Cuál de los dos estilos es mejor? No creo que se pueda decir que uno sea superior al otro; cada caso puede sugerir la conveniencia de un estilo u otro y cada programador en función de su experiencia también puede trabajar mejor con uno u otro. Sin embargo suele ser admitido que para realizar prototipos los lenguajes dinámicamente tipados son más adecuados mientras que para proyectos muy extensos, los estáticamente tipados son preferidos.

Typescript es un lenguaje de programación creado por Microsoft, que se distribuye con una licencia libre, y cuya intención principal es añadir a javascript la posibilidad de indicar los tipos de las variables. Typescript es “un superset (superconjunto) de javascript”. Lo que significa que cualquier código javascript válido es también un código typescript válido. Por tanto en Typescript podemos indicar el tipo de las variables que declaramos o no. Se deja a nuestro gusto. Por eso no podemos decir que sea estáticamente tipado, ya que indicar el tipo no es una exigencia. Pero si podemos utilizarlo como si lo fuera. De hecho es lo que se aconseja.

En esta sección mostraremos cómo instalar el compilador de javascript, cómo utilizarlo para generar código javascript y las características principales que añade: anotaciones de tipos, interfaces, clases mejoradas, genéricos, tipos union y constructores breves. Es importante sentirse cómodo con este lenguaje ya que es el que utilizaremos cuando programemos con Angular.

Instalación y uso

Typescript no cuenta con ningún intérprete o máquina virtual que lo ejecute directamente. Primero hay que convertirlo a código javascript y, entonces, ya se puede ejecutar en un entorno de ejecución que disponga de un intérprete de javascript, normalmente el browser o node.js.

La conversión de Typescript a javascript se denomina compilación y se realiza con una herramienta que se ejecuta en una interfaz de comandos: el compilador de Typescript. La instalación se hace con npm, el gestor de paquetes de node, escribiendo en una interfaz de comando la siguiente instrucción:

npm install -g typescript

La instalación añade dos comandos a sistema: tsc y tsserver. El primero es el compilador y el segundo es un servidor que incorpora el compilador y una serie de servicios del lenguajes que son útiles para los fabricantes de IDE’s que deseen ofrecer soporte de Typescript en sus entornos de desarrollo. A nosotros nos interesa el compilador. Veamos con un sencillo ejemplo cómo funciona.

Escribimos este sencillo programa en typescript. La extensión de los archivos con código typescript es .ts:

function suma(x: number, y: number): number { 
    return x + y; 
} 

let r: number = suma(3,6); 

console.log(r);

En este sencillo ejemplo ya puedes ver las anotaciones de los tipos de las variables. Si intentas ejecutar este programa con node.js verás que se producen errores ya que el intérprete de node.js no sabe leer typescript. Lo mismo ocurre si se hace desde un navegador web.

$ node prueba.ts SyntaxError: Unexpected token ...

Para poder ejecutar el código anterior, antes hemos de traducirlo a javascript usando el compilador tsc. Es tan fácil como hacer lo siguiente:

$ tsc prueba.ts

Al finalizar la compilación puedes comprobar que se ha generado un nuevo archivo denominado prueba.js que tiene la pinta siguiente:

function suma(x, y) { 
    return x + y; 
} 
var r = suma(3, 6); console.log(r);

Se trata de un archivo javascript válido equivalente que ya sí se puede ejecutar con cualquier intérprete javascript, por ejemplo con node.js:

$ node prueba.js 
$ 6

Como ejemplo de la detección de errores en los valores de los tipos detectado en tiempo de compilación, antes de la ejecución, podemos probar a usar una cadena en uno de los argumentos de la llamada a la función suma(). Si lo hacemos en el código javascript, veremos que se ejecutará sin problemas, aunque da un resultado que no es el esperado, ya que un número y una cadena no pueden sumarse matemáticamente hablando. El intérprete de javascript lo que hace es convertir el número en otra cadena y realizar la concatenación:

function suma(x, y) { 

    return x + y; 

} 

var r = suma(3, "hola"); 

console.log(r);

Al ejecutarlo:

$ node prueba.js 
$ 3hola

Sin embargo, si el error se comete en el código typescript:

function suma(x: number, y: number): number {
    return x + y; } 
let r: number = suma(3, "hola"); 
console.log(r);

Al compilarlo:

$ tsc prueba.ts prueba.ts:5:25 - error TS2345: Argument of type '"hola"' is not assignable to parameter of type 'number'. 5 let r: number = suma(3, "kaka"); ~~~~~~ Found 1 error.

Detecta el error en la asignación de uno de los parámetros de la función y nos indica el número de línea donde se produce, subrayando incluso el lugar dentro de la línea. Esto evita que pongamos la aplicación en producción o realicemos una distribución de la misma, pues estamos advertidos del error.

Sin embargo el compilador, a pesar de los errores, sigue ofreciendo una traducción. Lo curioso es que sigue traduciendo incluso si los errores son sintácticos, aunque en este caso el fichero javascript arrojará los pertinentes errores sintácticos en el momento de su ejecución.

El compilador de typescript puede ser configurado para que adopte distintos comportamientos. Escribiendo en la interfaz de comando tsc –help podemos ver la lista de opciones que podemos pasarle. Podemos hacer lo mismo consultando la documentación on-line.

Cuando utilicemos Angular, el propio framework se encargará de generar un código inicial con los ficheros de configuración debidamente afinados para compilar y ejecutar nuestro código de una manera prácticamente transparente. Por esta razón no tenemos que preocuparnos demasiado por conocer todas estas opciones. ¡Aunque no desanimo al que quiera seguir profundizando por su cuenta!

Características básicas de typescript

En esta sección describimos las características más importantes del lenguaje typescript. A saber; los tipos, las interfaces, los constructores breves, unión de tipos, alias de tipos y genéricos.

Tipos

Esta es la característica que más define al lenguaje. Tanto que forma parte de su propio nombre: typescript. Se reconocen los siguientes tipos básicos: boolean, number, string, arrays, tuple, enum, any, void, object. Mediante un ejemplo de código para cada tipo mostraremos la sintaxis de las anotaciones de tipo.

Boolean

Representa un valor lógico que puede ser true o false.

let haFinalizado: boolean = false;

Number

Representa un número que puede ser entero, float, octal, hexadecimal o binario.

let decimal: number = 6; 
let hex: number = 0xf00d; 
let binary: number = 0b1010; 
let octal: number = 0o744;

String

Representa una cadena alfanumérica.

let nombre: string = "Juanda" 
let apellido: string = 'Rodríguez'

Por supuesto podemos usar todas las manera que javascript ofrece para construir cadenas, desde el operador + para concatenar hasta las template literals introducidas en ES6.

Array

Representa una lista de elementos de cualquier tipo. Se especifica el tipo seguido de corchetes [].

let guarismos: number[] = [0, 1, 2, 3, 4, 5, 6]; 
let palabras: string[] = ["thing", "cosa", "chose"];
let funciones: Function[] = [x => x + 1, (x, y) => x - y];

Tupla

Un conjunto de elementos con tipos distintos.

let tupla: [number, string, boolean]; 
tupla = [3, "hola", true];

Enum

Es una manera de dar nombre descriptivos a un conjunto de números.

enum Color {Rojo, Verde, Azul} 
let c: Color = Color.Verde 
console.log(c); // Arroja 1

También podemos especificar los número de cada etiqueta:

enum Color {Rojo=0xFF0000, Verde=0x00FF00, Azul=0x0000FF} 
let c: Color = Color.Verde console.log(c); // Arroja 65280, que es el valor decimal de 0x00FF00

Any

Es un tipo que representa a cualquier otro tipo

let noSeguro: any = 4; 
noSeguro = "quizás una cadena";
noSeguro = false; // vale, mejor un booleano

Este tipo es útil cuando estamos migrando código javascript a typescript y vamos gradualmente comprobando qué valores se le van asignando a las variables en el código migrado.

Void

Es lo contrario que any, se trata de la ausencia de tipos. Se suele usar cuando se declaran funciones que no devuelven ningún valor, a las cuales se le debería llamar más propiamente procedimientos.

function pintaAlgo(): void{
    console.log('\\o/'); 
}

Object

Representa un objeto de javascript

let o: Object; o = {'clave': 'valor'}

Interfaces

Las interfaces en typescript van a permitirnos dos cosas. Por un lado crear nuevos tipos a partir de los existentes. Y por otro servir como modelo en la definición de clases que implementen la interfaz.

La primera funcionalidad, por sí sola, es realmente útil. Lo veremos con un ejemplo. Supongamos que estamos desarrollando una aplicación para inventariar productos de una tienda. Podemos crear un nuevo tipo Producto mediante una interfaz:

interface Producto { nombre: string; precio: number; }

Si queremos declarar una variable de tipo Producto:

let p: Producto;

Y garantizamos que en tiempo de compilación, antes de la ejecución del programa, la variable p consistirá en un objeto con sus atributos y tipos correspondientes. De esa manera podemos crear infinidad de nuevos tipos.

Pero además las interfaces de typescript son extensibles. Esto significa que podemos crear una interfaz más concreta a partir de una más general. Por ejemplo podemos crear una interfaz para representar libros y otra para representar ordenadores:

enum TipoLibro {Narrativa, Poesía, Ensayo} 
interface Libro extends Producto { 
    numPaginas: number; 
    titulo: string; 
    autor: string; 
    editorial: string; 
    tipo: TipoLibro; 
}
interface Ordenador extends Producto { 
    marca: string; 
    modelo: string; 
    cpu: string; 
    ram: number; 
}

Lo interesante de esto es que tanto las variables de tipo Libro como las de tipo Ordenador, además de los atributos propios de cada tipo, tendrán los atributos nombre (string) y precio (number) pues derivan de la interfaz más general Producto.

Es el mismo mecanismo de herencia que soportan las clases en cualquier lenguaje de programación orientado a objetos. Sin embargo, para utilizar provechosamente la interfaz, no tenemos la necesidad de definir ninguna clase que la implemente. Basta con considerarla como nuevos tipos que añadimos a nuestro gusto.

Por supuesto podemos crear interfaces con una estructura tan compleja como queramos, ampliando la jerarquía, es decir extendiendo a una interfaz que a su vez extiende a otra y así hasta el nivel que nos venga bien. Y también haciendo que los atributos de una interfaz sean del tipo representado por otra interfaz.

La segunda funcionalidad, la de implementar clases, es más típica y conocida. La finalidad es asegurar que una clase declare todas los atributos y métodos que se necesitan para ser utilizada en un determinado procedimiento.

Imaginemos que hemos decidido utilizar una librería que nos proporciona todos los abstrusos mecanismos que se utilizan en la autenticación. Y la documentación nos dice que podemos utilizar una función denominada checkUser() a la que debemos pasar como argumento un objeto que debe disponer de una función denominada generatePassword(), y otra encryptPassword(), ya que internamente la función checkUser() utiliza internamente dichos métodos del objeto usuario.

La solución para asegurarnos de que esto sea así consiste en que la librería en cuestión nos proporcione una interfaz como la siguiente:

interface IUser { 
    nombre: string; 
    apellidos: string; 
    username: string; 
    encryptPassword(): void generatePassword(): number;
    toString(): string; 
    setNombre(n: string): void;
    setApellido(a: string): void; 
}

Y nos aclare que la función checkUser() tiene la siguiente signatura:

function checkUser(user: IUser): boolean;

Si queremos usar dicha función con garantías tenemos que crear una clase que implemente todas los métodos que la interfaz IUser especifica. Por ejemplo:

class Usuario implements IUser { 
    nombre: string; 
    apellidos: string; 
    username: string; 
    constructor(u: string) { this.username = u; }
    encryptPassword(): void { // El código que encripta } 
    generatePassword(): number { // El código que genera el password }
    toString(): string { return this.nombre + ' ' + this.apellidos; } 
    setNombre(n: string): void { this.nombre = n; } 
    setApellido(a: string): void { this.apellidos = a; } 
}

Y ya podemos crear instancias de esa clase que pueden ser utilizadas correctamente  en la función checkUser().

Constructores breves

Es muy habitual que cuando declaramos clases, el constructor lleva como argumentos los valores de los atributos de la clase, y en el cuerpo del constructor se realizan las asignaciones correspondientes:

class Cubo { 
    private ancho: number; 
    private largo: number; 
    private profundo: number; 
    
    constructor(a: number, l: number, p: number){ 
        this.ancho = a;  
        this.largo = l; 
        this.profundo = p; 
    } 

    volumen(): number{ 
        return this.ancho * this.largo * this.profundo 
    }
}

let c = new Cubo(3, 5, 4); 
console.log(c.volumen()); // arroja 60

Los constructores breves de typescript nos proporcionan una manera más breve pero equivalente de hacer lo mismo (un syntactic sugar).

class Cubo{ 

    constructor(private ancho: number, private largo: number, private profundo: number){ } 
    volumen(): number{ 
         return this.ancho * this.largo * this.profundo 
    }
}

let c = new Cubo(3, 5, 4); 

console.log(c.volumen()); // arroja 60

Es decir, simplemente indicando las palabras claves private o public en los argumentos del constructor, el compilador entiende que deseamos crear unos atributos privados o públicos cuyos nombres sean los mismos que los de los argumentos y cuyos valores sean los que se especifiquen al instanciar el objeto. ¡Importante ahorro de código!

Unión de tipos

En algunas ocasiones nos vendrá bien que una variable pueda pertenecer a varios tipos distintos. Para ello podemos utilizar la unión de tipos. La idea es sencilla: si queremos que una variable pueda almacenar en unas ocasiones una cadena y en otras un número, la declararemos usando la unión de tipos.

let valor: string | number; valor = "hola"; 
valor = 5; 
valor = [1, 3]; Error de compilación

Alias de tipos

Con los alias de tipos podemos renombrar los tipos existentes o crear nuevos tipos a partir de los ya existentes de manera parecida a como lo hacemos con las interfaces. Veamos algunos ejemplos.

Podemos renombrar un tipo:

type Cadena = string;
let texto: Cadena = "hola";

O podemos crear uno nuevo uniendo dos existentes:

type Mixto = string | number let m: Mixto; m = 3;
m = "hola"

También podemos crear un tipo a partir de la definición de un objeto, algo prácticamente igual que lo que conseguimos con interfaces:

type Libro = { 
    titulo: string; 
    autor: string; 
    precio: number; 
}

Lo mismo podemos conseguir usando las interfaces. Y de hecho, para crear nuevos tipos a partir de la definición de objetos, es preferible usar las interfaces pues ofrecen más funcionalidades. Por ejemplo; los tipos definidos con type no pueden ser extendidos mientras que las interfaces sí.

Genéricos

Los tipos genéricos son muy usados en lenguajes orientados a objetos como Java, C++ o C#. La idea es que podamos construir clases , interfaces y  funciones capaces de trabajar con tipos que desconocemos a priori. Typescript también ofrece esta funcionalidad. La forma de utilizarlos, al igual que en los lenguajes que acabamos de mencionar, es indicando entre los signos < y > un identificativo que representa al tipo genérico.

Un ejemplo muy trivial para mostrar el uso de genéricos es el que sigue:

function identidad<T>(arg: T): T{ return arg; }

La función no hace más que devolver lo que le llega en el argumento, que gracias al genérico puede ser cualquier valor de  un tipo cualquiera.

Aquí se muestra un ejemplo ilustrativo del uso de genéricos.

Características del lenguaje typescript

Qué hemos aprendido

  • Typescript, es un superconjunto de javascript con anotación de tipos. Ni node.js ni el browser pueden interpretar directamente código typescript, por ello hay que traducirlo (compilarlo) previamente a javascript. Para ello se hace uso del compilador tsc. el cual se instala con npm install typescript -g.
  • Typescript ofrece la posibilidad de anotar las variables con tipos nativo y con tipos que podemos crear mediante:  interfaces, clases y alias de tipos.