TypesScript es un superset de JavaScript que le otorga características que lo hacen un lenguaje para escribir código mucho más robusto y escalable con el objetivo de tener la misma experencia de desarrollo de lenguajes más tradicionales como:
- Java
- C#
- Objective-C
Para conocer por que TypeScript es necesario para ser desarrollador web en estos momentos es necesario que conozcas las carencias de JavaScript.
JavaScript es un lenguaje que originalmente fue creado con el propósito de realizar ciertas operaciones en el lado de cliente cuando el internet todavía no era lo demasiado rápido en aquella época. Por lo consiguiente, JavaScript tuvo un desarrollo no muy bien planeado y en el desarrollo actual, sufre de la carencia de las siguientes características.
- Tipado de variables
- Errores en tiempo de escritura (linter)
- Autocompletado dependiendo de las variables
- Clases y Módulos (ES6)
- Validación de objetos dentro de objetos
- Tipado de respuesta HTTP
Debemos de conocer las características de TypeScript:
- Es un superset
- Es compatible
- Transpila a código JavaScript
- Añade clases en la OOP
Un superset nos ayuda a expandir las funcionalidades de un lenguaje, en este caso JavaScript, para lograr que este sea capaz de realizar el mismo trabajo de una forma más eficiente y ampliar el panorama de desarrollo.
Una cosa de la que no debes preocuparte es que todo el código de JavaScript es totalmente compatible en TypeScript, por lo que puedes ir aprendiendo TypeScript poco a poco e ir añadiendo sus mejores a medida que lo vayas dominando.
Todo el código escrito en TypeScript no es válido en el navegador web, por lo que necesitamos de alguna manera convertir el código escrito a JavaScript puro. Este proceso se conoce como transpilación. En el caso de JavaScript, todo el código es transpilado hacia una versión que es totalmente compatible con todos los navegadores.
TypeScript soporta de forma más robusta la orientación a objetos de forma similar a lenguajes como Java o C#.
No todas las características de TypeScript nos ortogan ventajas, asi que veamos algunas de las desventajas de implementar TypeScript en tu proyecto:
- Se tiene que transpilar
- Se tiene que escribir más código
TypeScript es un superset de JavaScript en un sentido sintactico: siempre y cuando tu programa de JavaScript no tenga errores, será también un programa de TypeScript válido.
Los archivos de TypeScript usan la extension .ts en lugar de .js de un archivo de JavaScript. Esto no significa que TypeScript sea un lenguaje diferente.
Esto es de enorme ayuda si estás migrando código de JavaScript a TypeScript. Significa que no tienes que reescribir algo de tu código a otro lenguaje para comenzar a utilizar TypeScript y obtener los beneficios que provee. Esto no sería verdad si eliges reescribir to código en un lenguaje como Java. Esta migración amable es uno de los mayores beneficios de TypeScript.
Todo los programas de JavaScript son programas de TypeScript válidos. Pero lo contrario no es cierto: hay programas de TypeScript que no son programas de JavaScript válido.
Esto es porque TypeScript añade una sintaxis adicional para especificar tipos.
Por ejemplo, este es un programa de TypeScript válido:
function greet(who:string) {
console.log(`Hello ${who}`);
}El who: string es una anotación de tipo que es específica de TypeScript.
Para compilar o generar nuestro archivo .js debemos de ejecutar el comando indicado para dicha tarea.
Ejecutamos por consola el comando tsc seguido del archivo de .ts que queremos compilar.
tsc <file.ts>
Para crear nuestro archivo de configuración de TypeScript utilizamos el siguiente comando:
tsc --init
Esto nos creará un nuevo archivo llamado tsconfig.json con toda la configuración disponible para TypeScript.
Cuando tenemos este archivo, compilar se vuelve más sencillo ya que no necesitamos indicar el nombre del archivo .ts que deseamos transpilar ya que esa información se encuentra en la configuración.
Cuando trabajamos en un proyecto, es muy común realizar múltiples cambios en diferentes archivos, pero en cada cambio debemos de realizar una compilación para poder verificar que nuestro código funciona. Entonces se puede volver demasiado tedioso tener que escribir el comando de compilación por consola cada vez que lo necesitemos.
Para solventar eso, podemos poner al compilador en el modo de observador con el siguiente comando.
tsc --watch
Este modo te permite decirle al compilador que cada vez que vea un cambio en los archivos del proyecto, realize la compilación correspondiente.
A alto nivel, debemos de entender que tsc (TypeScript compiler) hace 2 cosas:
- Convierte la siguiente generación de TypeScript/JavaScript a una versión más antigua de JavaScript que funciona en navegadores (transpilado).
- Inspecciona tu código por errores de tipos.
Algo muy importante que debes de entender es que la generación de código es independiente de los tipos, o dicho de otra manera, los tipos en tu código no afecta el JavaScript que TypeScript emite.
Debido a que la salida es independiente del chequeo de tipos, significa que el código con errores de tipos puede producir una salida.
Esto puede parecer un poco sorprendente debido a que si vienes de lenguajes como C o Java donde el chequeo de tipos y la salida van de la mano. Puedes pensar en todos los errores de TypeScript como en advertencias en esos lenguajes. Es parecido a indicar que existe una problema y que vale la pena investigarlo, pero no detiene la salida.
Mucho de las de configuración de TypeScript controlan donde buscar los archivos fuentes y que clase de código genera, pero unos pocos controlan el aspecto del lenguaje en si mismo. Esa son elecciones de diseño de alto nivel que la mayoría de los lenguajes no le permiten a sus usuarios. TypeScript se puede sentir como un lenguaje completamente diferente dependiendo de como este configurado. Para usarlo efectivamente, deberías comprender las más importantes de esas configuraciones: noImplicitAny y strictNullChecks
Controla si las variables deben de tener tipos conocidos. El siguiente código es válido cuando noImplicitAny está desactivado:
function add(a, b) {
return a + b
}Si revisas el tipo de la función add en tu editor, te revelará que tien TypeScript inferido sobre el tipo de esa función:
function add(a:any, b:any): anyEl tipo any desactiva efectivamente el inspector de tipos para el código que involucra esos parámetros. any es una herramienta útil, pero debería ser usada con preucación.
Esos son llamados implicit anys debido a que tu nuncas has escrito la palabra any. Esto llega a ser un error cuando activas la opción noImplicitAny
Estos errores pueden ser arreglados escribiendo explicítamente declaraciones de tipos, ya sea :any o un tipo más específico.
function add(a: number, b: number) {
return a + b
}TypeScript es el más provechoso cuando tiene información de tipos, así que deberías de asegurarte de colocar noImplicitAny siempre que sea posible. Para nuevos proyectos, deberías de comenzar con noImplicitAny activado, asi escribes tus tipos a medida que escribes tu código. Desactivarlo solo es apropiado si estás transicionando un projecto de JavaScript a TypeScript.
Controla si null and undefined son valores permitidos en todos los tipos. El siguiente código es válido cuando esta opción está apagada:
const x:number = null // ok, null is a valid numberPero dispara un error cuando está habilitada:
const x:number = null // Type 'null' is not assignable to type numberUn error similar podría haber ocurrido si usabas undefined en lugar de null. Si quieres permitir null, puedes solucionar este error haciendo tu inteción explícita:
const x:number | null = nullSi no deseas permitir null, necesitarás localizar de donde viene y añadir ya sea una verificación o una afirmación.
const el = document.getElementById('status')
if( el ) {
el.textContent = 'Ready' // ok, null ha sido excluido
}
el.!textContent = 'Ready' // ok, hemos afirmado que el no es nullUna de las metas del sistema de tipo de TypeScript es detectar código que lanzará una excepción en tiempo de ejecución, sin tener que ejecutar tu código. Cuando escuches que TypeScript es describido como un sistema estatico de tipo, es que se refiero a esto.
TypeScript soporta los mismo tipos de datos que JavaScript, pero adicionalmente añade algunos nuevos que veremos más adelante.
Antes de ver cada uno de los tipos de datos, necesitamos comprender que es el tipado estricto.
El tipado estricto nos obliga a indicar cual es el tipo de dato que puede contener una variable en el momento de la declaración. De ese modo, si le reasignamos el valor de una variable por otro de un tipo de dato distinto arrogará un error.
En TypeScript soportamos los mismo tipos de datos que JavaScript que ya conocemos:
- String
- Number
- Boolean
- Symbol
- null
- undefined
Al igual que los primitivos, soportamos los mismo tipos de datos compuesto que son los siguientes:
- Objetos literales
- Arreglos
- Funciones
- Clases
Por eso es que unicamente nos enfocaremos en los nuevos tipos de datos agregados por TypeScript:
- Any
- Interfaces
- Genericos
- Tuplas
El tipo any es un tipo de dato de TypeScript el cual le dice a la variable declarada con este tipo que cualquier tipo de dato es válido.
Se recomienda que nunca se utilice este tipo de dato debido a que perdemos las características del tipado estricto en TypeScript. Su uso debe ser solamente de forma provisional, pero debe ser corregido más adelante por el tipo de dato correcto.
// bad
let msg:any = '';
// Good
let username:string = '';Los array funcionan de la misma manera que en JavaScript pero con la diferencia de que TypeScript no nos dejará insertar un tipo de dato que no sea permitido, es decir, que si tenemos un arreglo que contienen strings, no podemos insertarle un dato númerico.
const cities = ['Mexico', 'Jalisco', 'Cancún', 'Tuxtla Gutierrez'];
cities.push(4) // => Wrong!!Pero podemos inicializar el arreglo vacio e indicarle cuales serán los tipos de datos que podrán ser agregados dentro del arreglo.
const arr:(string || number || boolean)[] = [];Crear un arreglo con distintos tipos de datos puede ser considerado una mala práctica, debido a eso, lo mejor es indicarle solo un tipo:
const numbers:number[] = [];Una tupla en TypeScript es un array de elementos que están tipados. De esta manera cada vez que haya que insertar un elemento se validará que dicho elemento coincida con el tipo de dato establecido en la tupla.
const hero:[string, number] = ['Dr strange', 100];Las enumeraciones son una de las pocas características que tiene TypeScript que no es una extensión de JavaScript de nivel de tipo.
Las enumeraciones permiten a un desarrollador definir un conjunto de constantes con nombre. El uso de enumeraciones puede hacer que sea más fácil documentar la intención o crear un conjunto de casos distintos. TypeScript proporciona enumeraciones numéricas y basadas en cadenas.
enum Direction {
Up = 1,
Down,
Left,
Right
}Arriba, tenemos una enumeración numérica donde Up se inicializa con 1. Todos los siguientes miembros se incrementan automáticamente a partir de ese punto. En otras palabras, Direction.Up tiene el valor 1, Down tiene 2, Left tiene 3 y Right tiene 4.
Las enumeraciones de cadena son un concepto similar, pero tienen algunas diferencias sutiles de tiempo de ejecución como se documenta a continuación. En una cadena de enumeración, cada miembro debe inicializarse constantemente con una cadena literal o con otro miembro de cadena de enumeración.
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}El tipo de dato void (vacio) es una forma que tiene el intérprete de TypeScript de indicar que una función no retornará ningún valor. Cuando una función no tiene un retorno explicíto, tendrá por defecto el valor de void (undefined en JavaScript). Es buena práctica indicar de forma explicita el valor de void cuando una función no retorne ningún valor, asi como en el siguiente ejemplo:
function printMessage(msg:any):void {
console.log(msg);
}Definir que una función retorna ningún valor te ayuda a documentar el código de proyecto y hace más fácil de comprender a tus funciones.
TypeScript introdujo un nuevo tipo never, que indica los valores que never ocurrirán.
El tipo never se usa cuando está seguro de que algo nunca sucederá. Por ejemplo, escribe una función que no volverá a su punto final o siempre arrojará una excepción.
const error = (msg:string):never => {
console.log(msg);
throw new Error(msg);
}
error('Auxilio!');El casting o casteo permite decirle al compilador de TypeScript que vas a tratar a cierto tipo de dato como otro tipo. Por ejemplo un uso común es cuando tenemos un valor de tipo any, pero nosotros sabemos que es un string, en ese caso se utiliza un casteo para indicar que ese valor de tipo any es un string.
const value:any = 'Foo bar';
const length:number = (value.length as string).length;Algo importante que debes de saber es que los tipos en TypeScript no están disponibles en tiempo de ejecución. Esto quiere decir que no pueden ser usados como valores por el interpréte de JavaScript debido a que esos valores no existen en el mismo JavaScript. Solo existen en el código TypeScript que por supuesto no es ejecutado.
Debido a eso, no podemos utilizar tipos o interfaces para realizar comprobaciones en JavaScript donde se espera un valor real.
interface Rectangle {
width: number;
height: number;
}
const rectangle: Rectangle = {
width: 10, height: 5,
}
rectangle instanceof Rectangle // => 'Rectangle' solo hace referencia a un tipo, pero aquí se usa como valor.Otro mito que muchos desarrolladores piensan es que los tipos de TypeScript tienen efecto alguno en el rendimiento final de un programa, lo cual es incorrecto.
La razón es que el código de TypeScript no es ejecutado en el cliente, si no el JavaScript que es producido por el.
El unico motivo por el cual pueda haber algún sobrecosto de rendimiento en un código generado por TypeScript sería cuando se genera algún código anterior que versus una implementación nativa. Esto quiere decir que TypeScript puede generar código que no está siendo soportando por navegadores antiguos seg;un el target indicado en la configuración de tsconfig.json. Cuando esto sucede, se podría dar el caso de que la salido del código generado sea menos eficienciente que el código nativo de un navegador que ya tiene esa implementación. Pero eso tiene poco o nada que ver como los tipos, ya que como vimos la generación de código es independiente de los tipos.
TypeScript puede inferir el tipo de datos cuando este no es explícitamente anotado en su declaración de tipo. Por ejemplo:
let city = 'new york city'En el ejemplo anterior no indicamos el tipo de dato en cuestión, sin embargo, no necesitas hacerlo ya que TypeScript infiere que el tipo de ciudad es string. Lo infiere por medio de su valor inicial.
Una de las metas del sistema de tipos es detectar el código que lanzará una excepción en tiempo de ejecución sin tener que ejecutar tu código. Cuando escuchas que describen a TypeScript como un sistema de tipo "estatico", es que se refiere a esto. El inspector de tipos no puede siempre destacar el código que lanzará excepciones, pero tratará.
Las funciones de TypeScript tienen unas características importantes que no tiene las funciones tradicionales de JavaScript. Por ejemplo podemos indicar el tipo de valor de los parámetros de que recibe una función.
function sayHello(msg:string) {
console.log(msg);
}Este tipado estricto obliga a los desarrolladores a utilizar las funciones como están diseñadas originalmente, permitiendo menos flexibilidad pero ganando peso en un código más robusto y documentado. Gracias a las bondades de TypeScript ahora somos capaces de saber cuales son los tipos que recibe la función ahorrandonos tiempo en saber como funciona o se comporta la misma.
Todas las funciones deben de retorna un valor por defecto. En el caso de JavaScript, ese valor es undefined, pero en TypeScript se recomienda utilizar el tipo de dato void para indicar que un función tiene un valor de retorno vacío.
Mejorando la función anterior:
function sayHello(msg):void {
console.log(msg);
return void;
}Cuando indicamos el tipo de dato que recibe el parámetro de una función también estamos indicando que dicho parámetro es obligatorio, ya que si no pasamos el valor correspondiente a dicho parámetro el compilador de TypeScript lanzará un error.
Para señalar que el parámetro es opcional, lo indicamos como el simbolo ?.
function sayHello(msg:string, upper:boolean):void {
msg = (upper) ? msg.toUpperCase() : msg;
console.log(msg);
return void;
}En TypeScript existe el tipo de dato función que básicamente significa que el valor que tendrá un variable solo podrá ser una función.
let greet:Function;Adicionalmente podemos ser más específicos y crear una estructura más compleja en la que indicamos ciertas características que tendrá dicha función.
// También podemos hacerlo con la sintaxis de arrow function
let greet:(msg:string) => void; Algo que debes de entender sobre los tipos de TypeScript es que los tipos declarados pueden ser diferentes a los tipos en tiempo de ejecución.
Lenguajes como C++ te permite definir multiples versiones de una función que difieren solo en los tipos de sus parámetros. Esto es llamado sobrecarga de funciones. Debido a que el comportamiento en tiempo de ejecución de tu código es independiente de los tipos de TypeScript, este constructo no es posible en TypeScript.
Sin embargo, TypeScript si proporciona una facilidad parecida, pero opera enteramente al nivel de tipo. Puedes proporcionar multiples declaraciones para un función, pero solo una implementación:
function add(a:number, b:number): number;
function add(a:string, b:number): string;
function add(a, b) {
return a + b
}
const three = add(1,2) // => 3
const twelve = add('1', 2) // => '12'El tipado estructural (structural typing) es una característica habilitada en TypeScript que permite que un objeto sea compatible con un tipo de objeto distinto si cumple con las estructura declarada. Cuando se espera un objeto de cierto tipo en una función, podemos pasarle otro que cumpla con las propiedades de su tipo declarado si importar que sea un tipo diferentes, como por ejemplo cuando tiene más propiedades de las necesarias.
Por ejemplo el siguiente tipo:
interface Vector2D {
x: number;
y: number;
}Puedes escribir una función para calcular su longitud:
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y)
}La función anterior recibe como parámetro un argumento del tipo Vector2D declarado anteriormente, pero podemos comprobar que puede recibir otro tipos tipos diferentes, siempre y cuando se cumpla con la estructura de Vector2D.
Ejemplo:
interface NamedVector {
name: string;
x: number;
y: number;
}La función calculateLength puede funcionar con un objeto NamedVector debido a que tiene una propiedad x y y que son números.
const v:NamedVector = { x: 3, y: 4, name: 'Zee' }
calculateLength(v) // okEsto es interesante debido a que no declaraste una relación entre Vector2D y NamedVector. No tienes que escribir una implementación alternativa de calculateLength para NamedVector. Esto es posible porque la estructura de NamedVector fue compatible con Vector2D.
Los types son tipos personalizados que te permiten crear estructuras definidas dentro de objetos. Es útil para verificar que todos los objetos cumplan con la estructura definida.
type Auto = {
carroceria: string,
modelo: string,
puertas: number,
pasajeros: number,
conducir?: () => void,
}Las classes en TypeScript son mucho más complejas que las classes del estándar ES6, ya que cuentan con características que se asemejan más a la programación orientada a objetos de otros lenguajes como C# o Java.
Para crear una clase utilizamos la palabra reservada class seguido del nombre de la clase en notación UpperCamelCase.
Ejemplo:
class Auto {
// ... propiedades
}Existen diferentes tipos de propiedades y métodos clasificados según su nivel de accesibilidad.
- Públicas: Son accedidas desde cualquier instancia de la clase o dentro de la misma
- Privadas: Son accedidas únicamente dentro de la definición de la clase
- Estáticas: Pueden ser accedidas utilizando como referencia el objeto de la clase en si misma sin necesidad realizar ninguna instancia.
- Protegidas: Son accedidas solamente en la definición de la clase y dentro de aquellas clase que son extendidas o heredadas
Al momento de declarar una propiedad o un método es necesario especificar cual es su correspondiente tipo de accesibilidad con su respectiva keyword:
- public
- private
- static
- protected
class User {
private password:string;
public username:string;
static getId = 'abc123';
protected saludar(){ return `Hola me llamo ${this.username}`}
constructor(password:string, username:string) {
this.password = password;
this.username = username;
}
}Las propiedades opcionales son aquellas que no son requeridas al momento de realizar la instancia de una clase, por lo que necesitamos indicar en el método constructor mediante el operador ?.
class User {
public username:string;
public email: string;
private password: string;
private numberPhone?: string;
constructor(username:string, email:string, password:string, numberPhone?:number) {
this.username = username;
this.email = email;
this.password = password;
this.numberPhone = numberPhone;
}
}Existe una forma de realizar una asignacíon de propiedades corta dentro de una clase utilizando los parámetros del método constructor. Con esto podemos simplificar el anterior ejemplo a lo siguiente:
class User {
constructor (
public username:string,
public email:string,
private password:string,
private numberPhone?:number
) {}
}La herencia de TypeScript funciona de forma muy similar a la de JavaScript. En ambas debemos de utilizar la keyword extends para indicar cual es la clase base de la cual vamos a heredar los métodos y propiedades.
class Person {
constructor(
public name: string,
public lastName: string,
){}
private getFullname() {
return `${ this.name } ${this.lastName}`
}
}
class Employee extends Person {}
const employee = new Employee('Donato', 'Monzón');
console.log(employee);Si observamos el resultado por consola vemos que obtenemos la nueva instancia wolverine que hereda desde la clase Avenger, pero sin necesidad de llamar al método super como se hace en JavaScript.
Esto sucede por que el constructor de la clase Xmen no existe, entonces TypeScript llama al constructor de la clase Avenger en su lugar.
Si necesitamos utilizar el método constructor de nuestra clase Xmen entonces será necesario llamar al método super().
class Avenger {
constructor(
public name: string,
public realName: string,
){
console.log('Constructor Avenger llamado');
}
private getFullname() {
return `${ this.name } ${this.realName}`;
}
}
class Xmen extends Avenger {
constructor (
name:string,
realName:string,
public isMutant:boolean,
) {
super(name, realName);
console.log('constructor Xmen llamado');
}
}
const wolverine = new Xmen('Wolverine', 'Logan', true);
console.log(wolverine);Una clase abstracta se declara con la keyword abstract antes de la definición de nuestra clase. La principal diferencia con las clases regulares es que las clases abstractas no pueden ser instanciadas, es decir, no podemos crear una variable o valor invocando directamente la clase por medio de la keyword new. En su lugar, el objetivo de la clase abstracta es servir como clase padre de la cual podamos heredar, con el propósito de tener una clase generica de base para crear clases más complejas.
abstract class Worker {
constructor(
public name:string,
public position:string,
) {}
}
class Employee extends Worker {};
const donato = new Employee('Donato', 'Developer');
console.log(donato);La interfaces es una forma que existe en TypeScript capaz de modelar objetos de forma muy similar a los types aliases. De hecho, las interfaces se parecen tanto a los types que se utilizan para el mismo propósito, pero hay una principal diferencia.
Las interfaces pueden ser extendibles, es decir, podemos crear una intefaz base y crear otra que va a heredar sus propiedades de forma muy similar a las clase.
Entonces, se podría definir a las interfaces entre el paso medio para una implementación entre los types y las clases.
interface Hero {
name: string;
age?: number;
powers: number;
getName?: () => string;
}Las interfaces te ayudan a definir y marcar la estructura de ciertos objetos, pero no necesariamente indican que los métodos y propiedades definidos han sido implementados.
Algunas veces tendremos objetos que son muy complejos, por ejemplo, objetos dentro de objetos. Realizar interfaces anidadas es una técnica que va a permitir crear objetos con estructuras complejas y bien definidas.
interface Cliente {
name:string;
age:number;
email:string;
address?: Address;
}
interface Address {
id: number;
zip: string;
city: string;
}
const client:Cliente = {
name: 'Pablo',
age: 25,
email: '[email protected]',
address: {
id: 123,
zip: '10000',
city: 'Tuxtla',
}
}Las interfaces también pueden servir para realizar la implementación de una clase, es decir, obligar a una clase a que tenga la estructura definida de una interfaz.
Para ello, utilizamos la keyword implements de forma similar a como aplicamos la herencia en extends:
interface SuperHero {
name: string;
realName: string;
power:string;
}
class Hero implements SuperHero{
constructor(
public name: string,
public realName: string,
public power: string
) {}
introduce() {
return `My name is ${this.name} and my power is ${this.power}`;
}
}Recapitulando, implements se utiliza en una clase para forzar que tengan implementados todos los métodos y propiedades definidos en una interfaz.