En el acelerado panorama digital actual, JavaScript se ha convertido en el lenguaje de referencia para crear aplicaciones web dinámicas. Sin embargo, la tipificación dinámica de JavaScript a veces puede dar lugar a errores sutiles, por lo que es difícil detectarlos al principio del proceso de desarrollo.

Ahí es donde entra TypeScript — para revolucionar la forma en que escribimos código JavaScript.

En este artículo, nos adentraremos en el mundo de TypeScript y exploraremos sus características, ventajas y mejores prácticas. También aprenderás cómo TypeScript aborda las limitaciones de JavaScript y libera el poder de la tipificación estática en la construcción de aplicaciones web robustas y escalables.

¡Vamos a sumergirnos!

¿Qué Es TypeScript?

TypeScript es un superconjunto de JavaScript que añade tipado estático opcional y funciones avanzadas a JavaScript. Ha sido desarrollado por Microsoft y se publicó por primera vez en octubre de 2012. Desde su lanzamiento en 2012, se ha extendido rápidamente entre la comunidad de desarrolladores web.

Según la encuesta a desarrolladores de Stack Overflow de 2022, TypeScript resultó ser la 4ª tecnología más querida, con un 73,46%. TypeScript se creó para abordar algunas de las limitaciones de JavaScript, como su falta de tipado fuerte, que puede dar lugar a errores sutiles difíciles de detectar durante el desarrollo.

Por ejemplo, considera el siguiente código JavaScript:

function add(a, b) {
  return a + b;
}

let result = add(10, "20"); // No error, but result is "1020" instead of 30

El código anterior crea una función add, que está tipada dinámicamente. El tipo de los argumentos a y b no se aplica. Como resultado, pasar una cadena en lugar de un número como argumento no produce un error, sino que concatena los valores como cadenas, provocando un comportamiento inesperado.

Con TypeScript, se introduce el tipado estático opcional, que permite a los desarrolladores especificar los tipos de variables, parámetros de función y valores de retorno, detectando errores relacionados con el tipo durante el desarrollo.

function add(a: number, b: number): number {
  return a + b;
}

let result = add(10, "20"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

En el código TypeScript anterior, los tipos de los parámetros a y b se definen explícitamente como números. Si se pasa una cadena como argumento, TypeScript generará un error en tiempo de compilación, proporcionando información temprana para detectar posibles problemas.

Características de TypeScript

TypeScript proporciona varias características potentes para el desarrollo web moderno que abordan algunas de las limitaciones de JavaScript. Estas características ofrecen una mejor experiencia para el desarrollador y una mejor organización del código. Entre ellas se incluyen:

1. Tipado Estático

TypeScript tiene un sólido sistema de tipado que permite especificar los tipos de variables y parámetros de funciones en tiempo de compilación. Esto permite la detección temprana de errores relacionados con el tipo, haciendo que el código sea más fiable y menos propenso a los bugs.

En JavaScript, en cambio, las variables están tipadas dinámicamente, lo que significa que su tipo puede cambiar en tiempo de ejecución.

Por ejemplo, el siguiente código JavaScript muestra la declaración de dos variables que están tipadas dinámicamente como número y cadena:

let num1 = 10; // num1 is dynamically typed as a number
let num2 = "20"; // num2 is dynamically typed as a string

let result = num1 + num2; // No error at compile-time
console.log(result); // Output: "1020"

Este código dará como resultado «1020», una concatenación de número y cadena. Esta no es la salida esperada — lo que puede afectar a tu código. La desventaja de JavaScript es que no arrojará ningún error. Puedes solucionarlo con TypeScript especificando los tipos de cada variable:

let num1: number = 10; // num1 is statically typed as a number
let num2: string = "20"; // num2 is statically typed as a string

let result = num1 + num2; // Error: Type 'string' is not assignable to type 'number'

En el código anterior, un intento de concatenar un número y una cadena utilizando el operador + produce un error en tiempo de compilación, ya que TypeScript aplica una comprobación de tipos estricta.

Esto ayuda a detectar posibles errores relacionados con el tipo antes de ejecutar el código, lo que conduce a un código más robusto y libre de errores.

2. Tipado Opcional

TypeScript proporciona flexibilidad a la hora de elegir entre utilizar tipado estático o no. Esto significa que puedes elegir especificar tipos para variables y parámetros de funciones o dejar que TypeScript infiera los tipos automáticamente basándose en el valor asignado.

Por ejemplo:

let num1: number = 10; // num1 is statically typed as a number
let num2 = "20"; // num2 is dynamically typed as a string

let result = num1 + num2; // Error: Operator '+' cannot be applied to types 'number' and 'string'

En este código, el tipo de num2 se infiere como string basándose en el valor asignado, pero puedes elegir especificar el tipo si lo deseas.

También puedes establecer el tipo a any, lo que significa que acepta cualquier tipo de valor:

let num1: number = 10;
let num2: any = "20";

let result = num1 + num2; // Error: Operator '+' cannot be applied to types 'number' and 'string'

3. Funciones ES6+

TypeScript es compatible con las funciones modernas de JavaScript, incluidas las introducidas en ECMAScript 6 (ES6) y versiones posteriores.

Esto permite a los desarrolladores escribir código más limpio y expresivo utilizando características como funciones de flecha, desestructuración, literales de plantilla, etc., con comprobación de tipos añadida.

Por ejemplo:

const greeting = (name: string): string => {
  return `Hello, ${name}!`; // Use of arrow function and template literal
};

console.log(greeting("John")); // Output: Hello, John!

En este código, la función arrow y el literal de plantilla se utilizan perfectamente. Lo mismo ocurre con toda la sintaxis de JavaScript.

4. Organización del Código

En JavaScript, organizar el código en archivos separados y gestionar las dependencias puede convertirse en un reto a medida que crece la base de código. Sin embargo, TypeScript proporciona soporte integrado para módulos y espacios de nombres para organizar mejor el código.

Los módulos permiten encapsular el código en archivos separados, facilitando la gestión y el mantenimiento de grandes bases de código.

Aquí tienes un ejemplo:

// greeting.ts:
export function greet(name: string): string { // Export a function from a module
  return `Hello, ${name}!`;
}

// app.ts:
import { greet } from "./greeting"; // Import from a module

console.log(greet("John")); // Output: Hello, John!

En el ejemplo anterior, tenemos dos archivos separados greeting.ts y app.ts. El archivo app.ts importa la función greet del archivo greeting.ts utilizando la declaración import. El archivo greeting.ts exporta la función greet utilizando la palabra clave export, haciéndola accesible para su importación en otros archivos.

Esto permite organizar mejor el código y separar las preocupaciones, facilitando la gestión y el mantenimiento de grandes bases de código.

Los espacios de nombres en TypeScript proporcionan una forma de agrupar código relacionado y evitar la contaminación global de espacios de nombres. Pueden utilizarse para definir un contenedor para un conjunto de clases, interfaces, funciones o variables relacionadas.

Aquí hay un ejemplo:

namespace Utilities {
  export function greet(name: string): string {
    return `Hello, ${name}!`;
  }
  
  export function capitalize(str: string): string {
    return str.toUpperCase();
  }
}

console.log(Utilities.greet("John")); // Output: Hello, John!
console.log(Utilities.capitalize("hello")); // Output: HELLO

En este código, definimos un namespace Utilities que contiene dos funciones, greet y capitalize. Podemos acceder a estas funciones utilizando el nombre del espacio de nombres seguido del nombre de la función, lo que proporciona una agrupación lógica para el código relacionado.

5. Características de la Programación Orientada a Objetos (OOP)

TypeScript admite conceptos de OOP como clases, interfaces y herencia, lo que permite un código estructurado y organizado.

Por ejemplo:

class Person {
  constructor(public name: string) {} // Define a class with a constructor
  greet(): string { // Define a method in a class
    return `Hello, my name is ${this.name}!`;
  }
}

const john = new Person("John"); // Create an instance of the class
console.log(john.greet()); // Output: Hello, my name is John!

6. Sistema de Tipos Avanzado

TypeScript proporciona un sistema de tipos avanzado que admite genéricos, uniones, intersecciones y mucho más. Estas características mejoran las capacidades de comprobación estática de tipos de TypeScript, permitiendo a los desarrolladores escribir código más robusto y expresivo.

Genéricos: Los genéricos permiten escribir código reutilizable que puede funcionar con distintos tipos. Los genéricos son como marcadores de tipos que se determinan en tiempo de ejecución basándose en los valores pasados a una función o una clase.

Por ejemplo, definamos una función genérica identidad que toma como argumento un valor del tipo T y devuelve un valor del mismo tipo T:

function identity(value: T): T {
  return value;
}

let num: number = identity(10); // T is inferred as number
let str: string = identity("hello"); // T is inferred as string

En el ejemplo anterior, el tipo T se infiere a partir del tipo de valor pasado a la función. En el primer uso de la función identidad, T se infiere como número porque pasamos 10 como argumento, y en el segundo uso, T se infiere como cadena porque pasamos "hello" como argumento.

Uniones e intersecciones: Las uniones e intersecciones se utilizan para componer tipos y crear relaciones de tipos más complejas.

Las uniones permiten combinar dos o más tipos en uno sólo que puede tener cualquiera de los tipos combinados. Las intersecciones permiten combinar dos o más tipos en sólo uno que debe satisfacer todos los tipos combinados.

Por ejemplo, podemos definir dos tipos Employee y Manager, que representan a un empleado y a un directivo, respectivamente.

type Employee = { name: string, role: string };
type Manager = { name: string, department: string };

Utilizando los tipos Employee y Manager, podemos definir un tipo de unión EmployeeOrManager que puede ser un Employee o un Manager.

type EmployeeOrManager = Employee | Manager; // Union type

let person1: EmployeeOrManager = { name: "John", role: "Developer" }; // Can be either Employee or Manager

En el código anterior, la variable person1 es del tipo EmployeeOrManager, lo que significa que se le puede asignar un objeto que satisfaga el tipo Employee o Manager.

También podemos definir un tipo de intersección EmployeeOrManager que debe satisfacer los tipos Employee y Manager.

type EmployeeAndManager = Employee & Manager; // Intersection type

let person2: EmployeeAndManager = { name: "Jane", role: "Manager", department: "HR" }; // Must be both Employee and Manager

En el código anterior, la variable person2 es del tipo EmployeeAndManager, lo que significa que debe ser un objeto que satisfaga los tipos Employee y Manager.

7. Compatibilidad con JavaScript

TypeScript está diseñado para ser un superconjunto de JavaScript, lo que significa que cualquier código JavaScript válido es también código TypeScript válido. Esto facilita la integración de TypeScript en proyectos JavaScript existentes sin tener que reescribir todo el código.

TypeScript se construye sobre JavaScript, añadiendo tipado estático opcional y características adicionales, pero aún te permite utilizar código JavaScript tal cual.

Por ejemplo, si tienes un archivo JavaScript app.js, puedes renombrarlo a app.ts y empezar a utilizar las características de TypeScript gradualmente sin cambiar el código JavaScript existente. TypeScript seguirá siendo capaz de entender y compilar el código JavaScript como TypeScript válido.

Aquí tines un ejemplo de cómo TypeScript proporciona una integración perfecta con JavaScript:

// app.js - Existing JavaScript code
function greet(name) {
  return "Hello, " + name + "!";
}

console.log(greet("John")); // Output: Hello, John!

Puedes cambiar el nombre del archivo JavaScript anterior a app.ts y empezar a utilizar las funciones de TypeScript:

// app.ts - Same JavaScript code as TypeScript
function greet(name: string): string {
  return "Hello, " + name + "!";
}

console.log(greet("John")); // Output: Hello, John!

En el ejemplo anterior, añadimos una anotación de tipo al parámetro name, especificándolo como string, que es opcional en TypeScript. El resto del código sigue siendo el mismo que en JavaScript. TypeScript es capaz de entender el código JavaScript y proporcionar comprobación de tipo para la anotación de tipo añadida, facilitando la adopción gradual de TypeScript en un proyecto JavaScript existente.

Cómo Empezar con TypeScript

TypeScript es un compilador oficial que puedes instalar en tu proyecto utilizando npm. Si quieres empezar a utilizar TypeScript 5.0 en tu proyecto, puedes ejecutar el siguiente comando en el directorio de tu proyecto:

npm install -D typescript

Esto instalará el compilador en el directorio node_modules, que ahora puedes ejecutar con el comando npx tsc.

Para tu proyecto JavaScript, primero tendrás que inicializar un proyecto node utilizando el siguiente comando para crear un archivo package.json:

npm init -y

A continuación, puedes instalar la dependencia de TypeScript, crear archivos TypeScript utilizando la extensión .ts y escribir tu código TypeScript.

Una vez que hayas escrito tu código TypeScript, tienes que compilarlo a JavaScript utilizando el compilador TypeScript (tsc). Puedes ejecutar el siguiente comando en el directorio de tu proyecto:

npx tsc .ts

Esto compila el código TypeScript en el archivo especificado a JavaScript y genera un archivo .js con el mismo nombre.

A continuación, puedes ejecutar el código JavaScript compilado en tu proyecto, del mismo modo que ejecutarías código JavaScript normal. Puedes utilizar Node.js para ejecutar el código JavaScript en un entorno Node.js o incluir el archivo JavaScript compilado en un archivo HTML y ejecutarlo en un navegador.

Trabajar con Interfaces

Las interfaces en TypeScript se utilizan para definir los contratos o la forma de los objetos. Te permiten especificar la estructura o forma a la que debe ajustarse un objeto.

Las interfaces definen un conjunto de propiedades y/o métodos que debe tener un objeto para que se considere compatible con la interfaz. Las interfaces pueden utilizarse para proporcionar anotaciones de tipo para objetos, parámetros de funciones y valores de retorno, lo que permite una mejor comprobación estática de tipos y sugerencias de finalización de código en los IDE.

Aquí hay un ejemplo de interfaz en TypeScript:

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

En este ejemplo, definimos una interfaz Person que especifica tres propiedades: firstName del tipo string, lastName del tipo string, y age del tipo number.

Cualquier objeto que tenga estas tres propiedades con los tipos especificados se considerará compatible con la interfaz Person. Definamos ahora objetos que se ajusten a la interfaz Person:

let person1: Person = {
  firstName: "John",
  lastName: "Doe",
  age: 30
};

let person2: Person = {
  firstName: "Jane",
  lastName: "Doe",
  age: 25
};

En este ejemplo, creamos dos objetos person1 y person2 que se ajustan a la interfaz Person. Ambos objetos tienen las propiedades requeridas firstName, lastName, y age con los tipos especificados, por lo que son compatibles con la interfaz Person.

Ampliación de Interfaces

Las interfaces también pueden ampliarse para crear nuevas interfaces que hereden propiedades de las interfaces existentes.

Por ejemplo:

interface Animal {
  name: string;
  sound: string;
}

interface Dog extends Animal {
  breed: string;
}

let dog: Dog = {
  name: "Buddy",
  sound: "Woof",
  breed: "Labrador"
};

En este ejemplo, definimos una interfaz Animal con las propiedades name y sound, y luego definimos una nueva interfaz «Dog» que amplía la interfaz Animal y añade una nueva propiedad breed. La interfaz Dog hereda las propiedades de la interfaz Animal, por lo que cualquier objeto que se ajuste a la interfaz Dog debe tener también las propiedades name y sound.

Propiedades Opcionales

Las interfaces también pueden tener propiedades opcionales, que se indican con un ? después del nombre de la propiedad.

Aquí tienes un ejemplo:

interface Car {
  make: string;
  model: string;
  year?: number;
}

let car1: Car = {
  make: "Toyota",
  model: "Camry"
};

let car2: Car = {
  make: "Honda",
  model: "Accord",
  year: 2020
};

En este ejemplo, definimos una interfaz Car con las propiedades make y model, y una propiedad opcional year. La propiedad year no es obligatoria, por lo que los objetos que se ajusten a la interfaz Car pueden tenerla o no.

Comprobación Avanzada de Tipos

TypeScript también proporciona opciones avanzadas para la comprobación de tipos en tsconfig.json. Estas opciones pueden mejorar las capacidades de comprobación de tipos de tu proyecto TypeScript y detectar posibles errores en tiempo de compilación, dando lugar a un código más robusto y fiable.

1. strictNullChecks

Cuando se establece en true, TypeScript aplica comprobaciones nulas estrictas, lo que significa que las variables no pueden tener un valor de null o undefined a menos que se especifique explícitamente con el tipo de unión null o undefined.

Por ejemplo:

{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

Con esta opción activada, TypeScript detectará los posibles valores null o undefined en tiempo de compilación, ayudando a evitar errores en tiempo de ejecución causados por el acceso a propiedades o métodos de las variables null o undefined.

// Example 1: Error - Object is possibly 'null'
let obj1: { prop: string } = null;
console.log(obj1.prop);

// Example 2: Error - Object is possibly 'undefined'
let obj2: { prop: string } = undefined;
console.log(obj2.prop);

2. strictFunctionTypes

Cuando se establece en true, TypeScript habilita la comprobación estricta de los tipos de función, incluyendo la bivarianza de los parámetros de función, lo que garantiza que los argumentos de función se comprueban estrictamente en cuanto a compatibilidad de tipos.

Por ejemplo:

{
  "compilerOptions": {
    "strictFunctionTypes": true
  }
}

Con esta opción activada, TypeScript detectará posibles desajustes de tipo de los parámetros de función en tiempo de compilación, ayudando a evitar errores en tiempo de ejecución causados por el paso de argumentos incorrectos a las funciones.

// Example: Error - Argument of type 'number' is not assignable to parameter of type 'string'
function greet(name: string) {
  console.log(`Hello, ${name}!`);
}

greet(123);

3. noImplicitarEsto

Cuando se establece a true, TypeScript no permite el uso de this con un tipo implícito any, lo que ayuda a detectar posibles errores al utilizar this en métodos de clase.

Por ejemplo:

{
  "compilerOptions": {
    "noImplicitThis": true
  }
}

Con esta opción activada, TypeScript detectará errores potenciales causados por el uso de this sin las anotaciones de tipo o vinculación adecuadas en métodos de clase.

// Example: Error - The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'
class MyClass {
  private prop: string;

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

  printProp() {
    console.log(this.prop);
  }
}

let obj = new MyClass("Hello");
setTimeout(obj.printProp, 1000); // 'this' context is lost, potential error

4. target

La opción target especifica la versión de destino ECMAScript para tu código TypeScript. Determina la versión de JavaScript que el compilador de TypeScript debe generar como salida.

Por ejemplo

{
  "compilerOptions": {
    "target": "ES2018"
  }
}

Con esta opción establecida en «ES2018«, TypeScript generará código JavaScript conforme al estándar ECMAScript 2018.

Esto puede ser útil si quieres aprovechar las últimas características y sintaxis de JavaScript, pero también necesitas garantizar la compatibilidad con entornos JavaScript más antiguos.

5. module

La opción module especifica el sistema de módulos que se utilizará en tu código TypeScript. Las opciones más comunes son «CommonJS«, «AMD«, «ES6«, «ES2015«, etc. Esto determina cómo se compilan tus módulos TypeScript en módulos JavaScript.

Por ejemplo:

{
  "compilerOptions": {
    "module": "ES6"
  }
}

Con esta opción establecida en «ES6«, TypeScript generará código JavaScript que utiliza la sintaxis de módulos ECMAScript 6.

Esto puede ser útil si estás trabajando con un entorno JavaScript moderno que admita módulos ECMAScript 6, como en una aplicación front-end que utilice un agrupador de módulos como webpack o Rollup.

6. noUnusedLocals y noUnusedParameters

Estas opciones permiten a TypeScript capturar las variables locales y los parámetros de función no utilizados, respectivamente.

Si se establecen en true, TypeScript emitirá errores de compilación para cualquier variable local o parámetro de función que se declare pero no se utilice en el código.

Por ejemplo:

{
  "compilerOptions": {
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

Estos son sólo algunos ejemplos más de opciones avanzadas de comprobación de tipos en el archivo tsconfig.json de TypeScript. Puedes consultar la documentación oficial para obtener más.

Buenas Prácticas y Consejos para Utilizar TypeScript

1. Anota Correctamente los Tipos de Variables, Parámetros de Funciones y Valores de Retorno

Una de las principales ventajas de TypeScript es su sólido sistema de tipado, que te permite especificar explícitamente los tipos de variables, parámetros de función y valores de retorno.

Esto mejora la legibilidad del código, detecta a tiempo posibles errores relacionados con los tipos y permite completar el código de forma inteligente en los IDE.

Aquí hay un ejemplo:

// Properly annotating variable types
let age: number = 25;
let name: string = "John";
let isStudent: boolean = false;
let scores: number[] = [98, 76, 89];
let person: { name: string, age: number } = { name: "John", age: 25 };

// Properly annotating function parameter and return types
function greet(name: string): string {
  return "Hello, " + name;
}

function add(a: number, b: number): number {
  return a + b;
}

2. Utilizar Eficazmente las Funciones Avanzadas de Tipos de TypeScript

TypeScript viene con un rico conjunto de características tipográficas avanzadas, como genéricos, uniones, intersecciones, tipos condicionales y tipos mapeados. Estas características pueden ayudarte a escribir código más flexible y reutilizable.

Aquí un ejemplo:

// Using generics to create a reusable function
function identity(value: T): T {
  return value;
}

let num: number = identity(42); // inferred type: number
let str: string = identity("hello"); // inferred type: string

// Using union types to allow multiple types
function display(value: number | string): void {
  console.log(value);
}

display(42); // valid
display("hello"); // valid
display(true); // error

3. Escribir Código Mantenible y Escalable con TypeScript

TypeScript fomenta la escritura de código mantenible y escalable proporcionando características como interfaces, clases y módulos.

Aquí hay un ejemplo:

// Using interfaces for defining contracts
interface Person {
  name: string;
  age: number;
}

function greet(person: Person): string {
  return "Hello, " + person.name;
}

let john: Person = { name: "John", age: 25 };
console.log(greet(john)); // "Hello, John"

// Using classes for encapsulation and abstraction
class Animal {
  constructor(private name: string, private species: string) {}

  public makeSound(): void {
    console.log("Animal is making a sound");
  }
}

class Dog extends Animal {
  constructor(name: string, breed: string) {
    super(name, "Dog");
    this.breed = breed;
  }

  public makeSound(): void {
    console.log("Dog is barking");
  }
}

let myDog: Dog = new Dog("Buddy", "Labrador");
myDog.makeSound(); // "Dog is barking"

4. Aprovechar las Herramientas y el Soporte IDE de TypeScript

TypeScript cuenta con excelentes herramientas y soporte IDE, con funciones como autocompletado, inferencia de tipos, refactorización y comprobación de errores.

Aprovecha estas funciones para mejorar tu productividad y detectar posibles errores en las primeras fases del proceso de desarrollo. Asegúrate de utilizar un IDE compatible con TypeScript, como Visual Studio Code, e instala el plugin TypeScript para una mejor experiencia de edición de código.

Extensión TypeScript de VS Code
Extensión TypeScript de VS Code

Resumen

TypeScript ofrece una amplia gama de potentes funciones que pueden mejorar enormemente tus proyectos de desarrollo web.

Su fuerte tipado estático, su avanzado sistema de tipos y sus capacidades de programación orientada a objetos lo convierten en una valiosa herramienta para escribir código mantenible, escalable y robusto. Las herramientas y el soporte IDE de TypeScript también proporcionan una experiencia de desarrollo sin fisuras.

Si quieres explorar TypeScript y sus capacidades, puedes hacerlo hoy mismo gracias al Alojamiento de Aplicaciones de Kinsta.