TypeScript 5.0 se lanzó oficialmente el 16 de marzo de 2023, y ya está disponible para todo el mundo. Esta versión introduce muchas características nuevas con el objetivo de hacer TypeScript más pequeño, más simple y más rápido.

Esta nueva versión moderniza los decoradores para la personalización de clases, permitiendo la personalización de clases y sus miembros de una forma reutilizable. Ahora los desarrolladores pueden añadir un modificador const a una declaración de parámetro de tipo, permitiendo que las inferencias tipo const sean las predeterminadas. La nueva versión también hace que todos los enums sean union enums, simplificando la estructura del código y acelerando la experiencia TypeScript.

En este artículo, explorarás los cambios introducidos en TypeScript 5.0, proporcionando una visión en profundidad de sus nuevas características y capacidades.

Primeros Pasos con TypeScript 5.0

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.

También puedes encontrar instrucciones sobre el uso de la nueva versión de TypeScript en Visual Studio Code en esta documentación.

¿Qué Novedades trae TypeScript 5.0?

En este artículo, vamos a explorar 5 actualizaciones importantes introducidas en TypeScript. Estas características incluyen:

Decoradores Modernizados

Los decoradores han existido en TypeScript durante un tiempo bajo una bandera experimental, pero la nueva versión los pone al día con la propuesta ECMAScript, que se encuentra ahora en la fase 3, lo que significa que está en una etapa en la que se añade a TypeScript.

Los decoradores son una forma de personalizar el comportamiento de las clases y sus miembros de forma reutilizable. Por ejemplo, si tienes una clase que tiene dos métodos, greet y getAge:

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }

    getAge() {
        console.log(`I am ${this.age} years old.`);
    }
}

const p = new Person('Ron', 30);
p.greet();
p.getAge();

En casos de uso en el mundo real, esta clase debería tener métodos más complicados que manejen alguna lógica asíncrona y tengan efectos paralelos, e.t.c., donde deberías lanzar algunas llamadas a console.log para ayudar a depurar los métodos.

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log('LOG: Method Execution Starts.');
        console.log(`Hello, my name is ${this.name}.`);
        console.log('LOG: Method Execution Ends.');
    }

    getAge() {
        console.log('LOG: Method Execution Starts.');
        console.log(`I am ${this.age} years old.`);
        console.log('Method Execution Ends.');
    }
}

const p = new Person('Ron', 30);
p.greet();
p.getAge();

Se trata de un patrón que se repite con frecuencia y sería conveniente disponer de una solución aplicable a todos los métodos.

Aquí es donde entran en juego los decoradores. Podemos definir una función llamada debugMethod que aparezca como sigue:

function debugMethod(originalMethod: any, context: any) {
    function replacementMethod(this: any, ...args: any[]) {
        console.log('Method Execution Starts.');
        const result = originalMethod.call(this, ...args);
        console.log('Method Execution Ends.');
        return result;
    }
    return replacementMethod;
}

En el código anterior, el debugMethod toma el método original (originalMethod) y devuelve una función que hace lo siguiente:

  1. Registra el mensaje «Comienza la Ejecución del Método».
  2. Pasa el método original y todos sus argumentos (incluido éste).
  3. Registra el mensaje «Finaliza la Ejecución del Método».
  4. Devuelve lo que haya devuelto el método original.

Utilizando decoradores, puedes aplicar el debugMethod a tus métodos, como se muestra en el código siguiente:

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    @debugMethod
    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
    @debugMethod
    getAge() {
        console.log(`I am ${this.age} years old.`);
    }
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();

Esto devolverá lo siguiente:

LOG: Entering method.
Hello, my name is Ron.
LOG: Exiting method.
LOG: Entering method.
I am 30 years old.
LOG: Exiting method.

Al definir la función decoradora (debugMethod), se pasa un segundo parámetro llamado context (es el objeto contexto — tiene información útil sobre cómo se declaró el método decorado y también el nombre del método). Puedes actualizar tu debugMethod para obtener el nombre del método del objeto context:

function debugMethod(
    originalMethod: any,
    context: ClassMethodDecoratorContext
) {
    const methodName = String(context.name);
    function replacementMethod(this: any, ...args: any[]) {
        console.log(`'${methodName}' Execution Starts.`);
        const result = originalMethod.call(this, ...args);
        console.log(`'${methodName}' Execution Ends.`);
        return result;
    }
    return replacementMethod;
}

Cuando ejecutes tu código, la salida llevará ahora el nombre de cada método decorado con el decorador debugMethod:

'greet' Execution Starts.
Hello, my name is Ron.
'greet' Execution Ends.
'getAge' Execution Starts.
I am 30 years old.
'getAge' Execution Ends.

Hay más cosas que puedes hacer con los decoradores. No dudes en consultar el pull request original para obtener más información sobre cómo utilizar los decoradores en TypeScript.

Introducción de los Parámetros de Tipo const

Este es otro gran lanzamiento que te proporciona una nueva herramienta con genéricos para mejorar la inferencia que obtienes cuando llamas a funciones. Por defecto, cuando declaras valores con const, TypeScript infiere el tipo y no sus valores literales:

// Inferred type: string[]
const names = ['John', 'Jake', 'Jack'];

Hasta ahora, para conseguir la inferencia deseada, tenías que utilizar la aserción const añadiendo «as const»:

// Inferred type: readonly ["John", "Jake", "Jack"]
const names = ['John', 'Jake', 'Jack'] as const;

Cuando llamas a funciones, es similar. En el código siguiente, el tipo inferido de los países es string[]:

type HasCountries = { countries: readonly string[] };
function getCountriesExactly(arg: T): T['countries'] {
    return arg.countries;
}

// Inferred type: string[]
const countries = getCountriesExactly({ countries: ['USA', 'Canada', 'India'] });

Puede que desees un tipo más específico, para lo cual una forma de solucionarlo hasta ahora ha sido añadir la aserción as const:

// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] } as const);

Esto puede ser difícil de recordar e implementar. Sin embargo, TypeScript 5.0 introduce una nueva característica por la que puedes añadir un modificador const a una declaración de parámetro de tipo, que aplicará automáticamente una inferencia de tipo const por defecto.

type HasCountries = { countries: readonly string[] };
function getNamesExactly(arg: T): T['countries'] {
    return arg.countries;
}

// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] });

El uso de parámetros de tipo const permite a los desarrolladores expresar la intención más claramente en su código. Si se pretende que una variable sea constante y no cambie nunca, utilizar un parámetro de tipo const garantiza que nunca pueda cambiarse accidentalmente.

Puedes consultar el pull request original para obtener más información sobre cómo funciona el parámetro de tipo const en TypeScript.

Mejoras en los Enums

Los enums en TypeScript son una potente construcción que permite a los desarrolladores definir un conjunto de constantes con nombre. En TypeScript 5.0, se han introducido mejoras en los enums para hacerlos aún más flexibles y útiles.

Por ejemplo, si pasas el siguiente enum a una función:

enum Color {
    Red,
    Green,
    Blue,
}

function getColorName(colorLevel: Color) {
    return colorLevel;
}

console.log(getColorName(1));

Antes de la introducción de TypeScript 5.0, podías pasar un número de nivel incorrecto, y no arrojaría ningún error. Pero con la introducción de TypeScript 5.0, se producirá inmediatamente un error.

Además, la nueva versión convierte todos los enums en enums de unión, creando un tipo único para cada miembro computado. Esta mejora permite acotar todos los enums y referenciar sus miembros como tipos:

enum Color {
    Red,
    Purple,
    Orange,
    Green,
    Blue,
    Black,
    White,
}

type PrimaryColor = Color.Red | Color.Green | Color.Blue;

function isPrimaryColor(c: Color): c is PrimaryColor {
    return c === Color.Red || c === Color.Green || c === Color.Blue;
}

console.log(isPrimaryColor(Color.White)); // Outputs: false
console.log(isPrimaryColor(Color.Red)); // Outputs: true

Mejoras de Rendimiento de TypeScript 5.0

TypeScript 5.0 incluye numerosos cambios significativos en la estructura del código, las estructuras de datos y las extensiones algorítmicas. Esto ha ayudado a mejorar toda la experiencia TypeScript, desde la instalación hasta la ejecución, haciéndola más rápida y eficiente.

Por ejemplo, la diferencia entre el tamaño de los paquetes de TypeScript 5.0 y 4.9 es bastante considerable.

TypeScript se ha migrado recientemente de espacios de nombres a módulos, lo que le permite aprovechar las herramientas de construcción modernas que pueden realizar optimizaciones como la elevación del ámbito. Además, la eliminación de código obsoleto ha reducido en 26,4 MB los 63,8 MB del paquete de TypeScript 4.9.

Tamaño del paquete TypeScript
Tamaño del paquete TypeScript

Aquí tienes algunas ventajas interesantes en velocidad y tamaño entre TypeScript 5.0 y 4.9:

Escenario Tiempo o tamaño en relación con TS 4.9
Tiempo de construcción material-ui 90%
Tiempo de inicio del Compilador TypeScript 89%
Tiempo de construcción de Playwright 88%
Tiempo de autocompilación del Compilador de TypeScript 87%
Tiempo de construcción de Outlook Web 82%
Tiempo de construcción de VS Code 80%
Tamaño del paquete typescript npm 59%

Resolución Bundler para una Mejor Resolución de Módulos

Cuando escribes una sentencia import en TypeScript, el compilador necesita saber a qué se refiere la importación. Para ello utiliza la resolución de módulos. Por ejemplo, cuando escribes import { a } from "moduleA", el compilador necesita conocer la definición de a en moduleA para comprobar su uso.

En TypeScript 4.7, se añadieron dos nuevas opciones para la configuración de --module y moduleResolution: node16 y nodenext.

El propósito de estas opciones era representar con mayor precisión las reglas exactas de búsqueda de módulos ECMAScript en Node.js. Sin embargo, este modo tiene varias restricciones que no aplican otras herramientas.

Por ejemplo, en un módulo ECMAScript en Node.js, cualquier importación relativa debe incluir una extensión de archivo para que funcione correctamente:

import * as utils from "./utils"; // Wrong 

import * as utils from "./utils.mjs"; // Correct

TypeScript ha introducido una nueva estrategia llamada «moduleResolution bundler». Esta estrategia puede implementarse añadiendo el siguiente código en la sección «compilerOptions» de tu archivo de configuración de TypeScript:

{
    "compilerOptions": {
        "target": "esnext",
        "moduleResolution": "bundler"
    }
}

Esta nueva estrategia es adecuada para aquellos que utilizan bundlers modernos como Vite, esbuild, swc, Webpack, Parcel y otros que utilizan una estrategia de búsqueda híbrida.

Puedes consultar el pull request original y su implementación para obtener más información sobre cómo funciona el bundler moduleResolution en TypeScript.

Depreciaciones

TypeScript 5.0 viene con algunas depreciaciones, incluyendo requisitos de tiempo de ejecución, cambios en lib.d.ts y cambios que rompen la API.

  1. Requisitos de Tiempo de Ejecución: TypeScript ahora está orientado a ECMAScript 2018, y el paquete establece una expectativa de motor mínima de 12.20. Por lo tanto, los usuarios de Node.js deben tener una versión mínima de 12.20 o posterior para utilizar TypeScript 5.0.
  2. Cambios en lib.d.ts: Se han producido algunos cambios en la forma de generar los tipos para el DOM, que pueden afectar al código existente. En concreto, algunas propiedades se han convertido de tipos numéricos a tipos literales numéricos, y las propiedades y métodos para la gestión de eventos de cortar, copiar y pegar se han trasladado entre interfaces.
  3. Cambios de ruptura de la API: Se han eliminado algunas interfaces innecesarias y se han realizado algunas mejoras de corrección. TypeScript 5.0 también se ha trasladado a módulos.

TypeScript 5.0 ha dejado obsoletos ciertos ajustes y sus valores correspondientes, incluyendo target: ES3, out, noImplicitUseStrict, keyofStringsOnly, suppressExcessPropertyErrors, suppressImplicitAnyIndexErrors, noStrictGenericChecks, charset, importsNotUsedAsValues, y preserveValueImports, así como prepend en las referencias de proyectos.

Aunque estas configuraciones seguirán siendo válidas hasta TypeScript 5.5, se emitirá una advertencia para alertar a los usuarios que aún las utilicen.

Resumen

En este artículo, has aprendido algunas de las principales características y mejoras que aporta TypeScript 5.0, como las mejoras en los enums, la resolución de bundler y los parámetros de tipo const, junto con mejoras en la velocidad y el tamaño.

Si estás pensando en TypeScript para tus próximos proyectos, prueba gratis el Alojamiento de Aplicaciones de Kinsta.

¡Ahora es tu turno! ¿Qué características o mejoras te parecen más atractivas en TypeScript 5.0? ¿Hay alguna significativa que hayamos pasado por alto? Háznoslo saber en los comentarios.

Joel Olawanle Kinsta

Joel es un desarrollador Frontend que trabaja en Kinsta como Editor Técnico. Es un formador apasionado enamorado del código abierto y ha escrito más de 200 artículos técnicos, principalmente sobre JavaScript y sus frameworks.