O TypeScript 5.0 foi lançado oficialmente em 16 de março de 2023 e já está disponível para todo mundo usar. A nova versão traz um monte de recursos inéditos com o foco em deixar o TypeScript mais leve, fácil de usar e rápido.

Este novo lançamento moderniza os decoradores para personalização de classes, permitindo a personalização das classes e seus membros de uma forma reutilizável. Os desenvolvedores podem agora adicionar um modificador de const a uma declaração de parâmetro de tipo, permitindo que inferências do const type sejam o padrão. A nova versão também faz com que todos os enums unam enums, simplificando a estrutura do código e acelerando a experiência do TypeScript.

Neste artigo, você irá explorar as mudanças introduzidas no TypeScript 5.0, fornecendo uma visão detalhada de suas novas características e capacidades.

Como começar com o TypeScript 5.0

O TypeScript é um compilador oficial que você pode instalar em seu projeto usando npm. Se você quiser começar a usar o TypeScript 5.0 em seu projeto, você pode executar o seguinte comando no diretório do seu projeto:

npm install -D typescript

Isso instalará o compilador no diretório node_modules, que agora você pode rodar usando o comando npx tsc.

Você também pode encontrar instruções sobre o uso da nova versão do TypeScript no Visual Studio Code nesta documentação.

O que há de novo no TypeScript 5.0?

Neste artigo, vamos explorar 5 grandes atualizações introduzidas no TypeScript. Esses recursos incluem:

Decoradores modernizados

Os decoradores já existem no TypeScript há algum tempo sob uma bandeira experimental, mas a nova versão os atualiza com a proposta do ECMAScript, que agora está no estágio 3, o que significa que está em um estágio em que é adicionado ao TypeScript.

Os decoradores são uma forma de personalizar o comportamento de classes e seus membros de uma maneira reutilizável. Por exemplo, se você tem uma classe que possui dois métodos, greet e 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();

Em casos de uso do mundo real, essa classe deveria ter métodos mais complexos que lidam com alguma lógica assíncrona com efeitos colaterais, etc., onde você gostaria de incluir alguns comandos console.log para depurar os 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();

Esse é um padrão que ocorre frequentemente, e seria conveniente ter uma solução para aplicar a cada método.

É aqui que os decoradores entram em cena. Podemos definir uma função chamada debugMethod que seria assim:

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;
}

No código acima, o debugMethod recebe o método original (originalMethod) e retorna uma função que faz o seguinte:

  1. Registra uma mensagem “Início da execução do método.”
  2. Passa o método original e todos os seus argumentos (incluindo this).
  3. Registra uma mensagem “Fim da execução do método.”
  4. Retorna o que o método original retornou.

Usando decoradores, você pode aplicar o debugMethod aos seus métodos como mostrado no código abaixo:

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();

Isso irá resultar no seguinte:

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

Ao definir a função do decorador, chamado de (debugMethod), um segundo parâmetro nomeado context é incluído. Esse objeto de contexto contém informações valiosas, como a forma como o método decorado foi criado e o nome do método em si. Você pode modificar o seu debugMethod para extrair o nome do método diretamente desse 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;
}

Quando você executar seu código, a saída agora terá o nome de cada método decorado com o decorador debugMethod:

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

Há mais sobre o que você pode fazer com os decoradores. Sinta-se livre para verificar o pedido pull request para mais informações sobre como usar decoradores em TypeScript.

Introduzindo os parâmetros do const type

Este é outro grande lançamento que lhe dá uma nova ferramenta com genéricos a fim de melhorar a inferência que você obtém quando você chama funções. Por padrão, quando você declara valores com const, o TypeScript infere o tipo e não seus valores literais:

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

Até agora, para conseguir a inferência desejada, você tinha que usar a afirmação “as const”:

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

Ao chamar funções, ocorre o mesmo. No código abaixo, o tipo inferido de countries é 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'] });

Talvez você deseje um tipo mais específico, e uma forma de corrigir isso anteriormente era adicionar a afirmação as const:

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

Isso pode ser difícil de lembrar e implementar. No entanto, o TypeScript 5.0 introduz um novo recurso onde você pode adicionar um modificador const a uma declaração de parâmetro de tipo, o que aplicará automaticamente uma inferência do const type como padrão.

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'] });

O uso de parâmetros doconst type permite que os desenvolvedores expressem suas intenções mais claramente em seu código. Se uma variável tiver a intenção de ser constante e nunca mudar, o uso de um parâmetro do const type garante que ela nunca poderá ser alterada acidentalmente.

Você pode verificar o pedido pull request para mais informações sobre como o parâmetro const type funciona no TypeScript.

Melhorias no Enums

Enums no TypeScript são uma construção poderosa que permite aos desenvolvedores definir um conjunto de constantes nomeadas. No TypeScript 5.0, foram feitas melhorias nos enums para torná-los ainda mais flexíveis e úteis.

Por exemplo, se você tiver o seguinte enum passado para uma função:

enum Color {
    Red,
    Green,
    Blue,
}

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

console.log(getColorName(1));

Antes da introdução do TypeScript 5.0, você poderia passar um número de nível errado, e isso não geraria nenhum erro. Mas com a introdução do TypeScript 5.0, ele imediatamente gerará um erro.

Além disso, a nova versão transforma todos os enums em enums de união, criando um tipo único para cada membro calculado. Esse aprimoramento permite estreitar todos os enums e a referência aos seus membros 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

Melhorias de desempenho do TypeScript 5.0

O TypeScript 5.0 inclui diversas mudanças significativas na estrutura de código, estruturas de dados e extensões algorítmicas. Isso ajudou a melhorar toda a experiência do TypeScript, desde a instalação até a execução, tornando mais rápido e eficiente.

Por exemplo, a diferença entre o tamanho do pacote do TypeScript 5.0 e 4.9 é bastante impressionante.

TypeScript foi recentemente migrado de namespaces para módulos, permitindo que ele aproveite as modernas ferramentas de build que podem realizar otimizações como o levantamento de escopo. Além disso, a remoção de algum código obsoleto reduziu cerca de 26.4 MB do tamanho de 63.8 MB do TypeScript 4.9.

TypeScript package size
Tamanho do pacote TypeScript

Aqui estão mais alguns ganhos interessantes em velocidade e tamanho entre TypeScript 5.0 e 4.9:

Cenário Tempo ou Tamanho Relativo ao TS 4.9
Tempo de build  material-ui 90%
Tempo de inicialização do compilador TypeScript 89%
Tempo de build Playwright 88%
Tempo de self-build do compilador TypeScript 87%
Tempo de build  do Outlook Web 82%
Tempo de build  do código VS 80%
Tamanho do Pacote npm do TypeScript 59%

Resolução de Bundler para melhor resolução do módulo

Quando você escreve uma instrução de importação no TypeScript, o compilador precisa saber a que se refere essa importação. Ele faz isso usando a resolução de módulos. Por exemplo, quando você escreve import { a } from "moduleA", o compilador precisa saber a definição de “a” em moduleA para verificar seu uso.

No TypeScript 4.7, duas novas opções foram adicionadas para as configurações --module e moduleResolution: node16 e nodenext.

O propósito dessas opções era representar com mais precisão as regras exatas de pesquisa dos módulos ECMAScript no Node.js. No entanto, este modo tem várias restrições que não são aplicadas por outras ferramentas.

Por exemplo, em um módulo ECMAScript no Node.js, qualquer importação relativa deve incluir uma extensão de arquivo para que ele funcione corretamente:

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

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

O TypeScript introduziu uma nova estratégia chamada “moduleResolution bundler” Esta estratégia pode ser implementada adicionando o seguinte código na seção “compilerOptions” do seu arquivo de configuração TypeScript:

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

Esta nova estratégia é adequada para aqueles que utilizam bundlers modernos como Vite, esbuild, swc, Webpack, Parcel, e outros que utilizam uma estratégia de pesquisa híbrida.

Você pode verificar o pull request original e sua implementação para mais informações sobre como o moduleResolution bundler funciona no TypeScript.

Depreciações

O TypeScript 5.0 vem com alguma depreciação, incluindo requisitos de tempo de execução, mudanças na lib.d.ts e mudanças de quebra de API.

  1. Requisitos de tempo de execução: O TypeScript agora tem como alvo ECMAScript 2018, e o pacote estabelece uma expectativa mínima de mecanismo de 12.20. Portanto, os usuários do Node.js devem ter uma versão mínima de 12.20 ou posterior para usar o TypeScript 5.0.
  2. Mudanças na lib.d.ts: Houve algumas mudanças na forma como os tipos para o DOM são gerados, o que pode afetar o código existente. Em particular, certas propriedades foram convertidas de número para tipos literais numéricos, e propriedades e métodos para manipulação de eventos de cortar, copiar e colar foram movidos entre interfaces.
  3. Mudanças de quebra de API: Algumas interfaces desnecessárias foram removidas, e algumas melhorias de correção foram feitas. O TypeScript 5.0 também foi movido para módulos.

O TypeScript 5.0 depreciou certas configurações e seus valores correspondentes, incluindo target: ES3, out, noImplicitUseStrict, keyofStringsOnly, suppressExcessPropertyErrors, suppressImplicitAnyIndexErrors, noStrictGenericChecks, charset, importsNotUsedAsValues, e preserveValueImports, bem como prepend em referências de projeto.

Enquanto essas configurações permanecerão válidas até o TypeScript 5.5, um aviso será emitido para alertar os usuários que ainda as utilizam.

Resumo

Neste artigo, você aprendeu algumas das principais funcionalidades e melhorias que o TypeScript 5.0 traz, como melhorias em enums, resolução de bundler e parâmetros de const type, além de melhorias na velocidade e tamanho.

Se você está pensando em TypeScript para seus próximos projetos, experimente a Hospedagem de Aplicativos da Kinsta gratuitamente.

Agora é a sua vez! Quais funcionalidades ou melhorias você acha mais interessantes no TypeScript 5.0? Existe algum recurso ou melhoria importante que pode ter sido deixado de lado? Compartilhe sua opinião nos comentários.

Joel Olawanle Kinsta

Joel is a Frontend developer working at Kinsta as a Technical Editor. He is a passionate teacher with love for open source and has written over 200 technical articles majorly around JavaScript and it's frameworks.