No cenário digital acelerado de hoje, o JavaScript se tornou a linguagem preferida para construir aplicativos web dinâmicas. No entanto, a digitação dinâmica do JavaScript às vezes pode levar a erros sutis, tornando desafiador identificá-los precocemente no processo de desenvolvimento.

É aí que entra o TypeScript, para revolucionar a maneira como escrevemos o código JavaScript.

Neste artigo, vamos explorar detalhadamente o mundo do TypeScript e examinar suas características, vantagens e melhores práticas. Você também aprenderá como o TypeScript enfrenta as limitações do JavaScript e desbloqueia o poder da digitação estática na construção de aplicativos web robustas e escaláveis.

Vamos começar!

O que é TypeScript?

TypeScript é um superconjunto de JavaScript que adiciona digitação estática opcional e recursos avançados ao JavaScript. Foi desenvolvido pela Microsoft e lançado inicialmente em outubro de 2012. Desde o seu lançamento em 2012, ele ganhou rapidamente ampla adoção na comunidade de desenvolvimento web.

De acordo com a pesquisa de desenvolvedores Stack Overflow de 2022, TypeScript emergiu como a 4ª tecnologia mais amada, com 73,46%. TypeScript foi criado para resolver algumas das limitações do JavaScript, como sua falta de digitação forte, que pode levar a erros sutis que são difíceis de pegar durante o desenvolvimento.

Por exemplo, considere o seguinte código JavaScript:

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

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

O código acima cria uma função add, cuja digitação é dinâmica. O tipo dos argumentos a e b não é aplicado. Como resultado, passar uma string de caracteres em vez de um número como argumento não produz um erro, mas concatena os valores como strings de caracteres, levando a um comportamento inesperado.

Com o TypeScript, a digitação estática opcional é introduzida, permitindo que os desenvolvedores especifiquem os tipos de variáveis, parâmetros de função e valores de retorno, detectando erros relacionados a tipos durante o desenvolvimento.

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'

No código TypeScript acima, os tipos de parâmetros a e b são explicitamente definidos como números. Se uma string for passada como argumento, o TypeScript irá gerar um erro de tempo de build, fornecendo feedback antecipado para detectar possíveis problemas.

Recursos do TypeScript

O TypeScript oferece vários recursos avançados para o desenvolvimento moderno da web que abordam algumas das limitações do JavaScript. Esses recursos oferecem experiência aprimorada para o desenvolvedor e organização do código. Eles incluem:

1. Digitação estática

O TypeScript tem um forte sistema de digitação que permite especificar os tipos de variáveis e parâmetros de função em runtime. Isso permite a detecção precoce de erros relacionados ao tipo, tornando o código mais confiável e menos propenso a bugs.

No JavaScript, por outro lado, a digitação das variáveis é dinâmica, o que significa que seu tipo pode mudar durante o tempo de execução.

Por exemplo, o código JavaScript abaixo mostra a declaração de duas variáveis que são dinamicamente digitadas como número e string:

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"

O código acima produzirá “1020”, uma concatenação de número e string de caracteres. Essa não é a saída esperada, o que significa que isso pode afetar seu código. A desvantagem do JavaScript é que ele não gera nenhum erro. Você pode corrigir isso com o TypeScript especificando os tipos de cada variável:

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'

No código acima, uma tentativa de concatenar um número e uma string de caracteres usando o operador + resulta em um erro runtime, pois o TypeScript impõe uma verificação rigorosa do tipo.

Isso ajuda a detectar possíveis erros relacionados a tipos antes de executar o código, resultando em um código mais robusto e livre de erros.

2. Digitação opcional

O TypeScript oferece flexibilidade para que você escolha usar ou não a digitação estática. Isso significa que você pode optar por especificar tipos para variáveis e parâmetros de função ou deixar o TypeScript inferir os tipos automaticamente com base no valor atribuído.

Por exemplo:

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'

Neste código, o tipo de num2 é inferido como string com base no valor atribuído, mas você pode optar por especificar o tipo, se desejar.

Você também pode definir o tipo como any, o que significa que ele aceita qualquer 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. Recursos ES6+

O TypeScript oferece suporte para recursos modernos de JavaScript, incluindo aqueles introduzidos no ECMAScript 6 (ES6) e versões posteriores.

Isso permite que os desenvolvedores escrevam códigos mais limpos e expressivos usando recursos como funções de seta, desestruturação, literais de modelo e muito mais, com verificação de tipo adicional.

Por exemplo:

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

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

Neste código, a função de seta e o literal de modelo são usados perfeitamente. O mesmo se aplica a toda a sintaxe do JavaScript.

4. Organização do código

No JavaScript, a organização do código em arquivos separados e o gerenciamento de dependências podem se tornar um desafio à medida que a base de código cresce. No entanto, o TypeScript oferece suporte integrado para módulos e namespaces para que você organize melhor o código.

Os módulos permitem o encapsulamento do código em arquivos separados, facilitando o gerenciamento e a manutenção de grandes bases de código.

Aqui está um exemplo:

// 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!

No exemplo acima, temos dois arquivos separados: greeting.ts e app.ts. O arquivo app.ts importa a função greet do arquivo greeting.ts usando a instrução import. O arquivo greeting.ts exporta a função greet usando a palavra-chave export, tornando acessível para importação em outros arquivos.

Isso permite uma melhor organização do código e a separação das preocupações, facilitando o gerenciamento e a manutenção de grandes bases de código.

Os namespaces no TypeScript oferecem uma maneira de agrupar códigos relacionados e evitar a poluição global de namespaces. Eles podem ser usados para definir um contêiner para um conjunto de classes, interfaces, funções ou variáveis relacionadas.

Aqui está um exemplo:

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

Neste código, definimos um namespace Utilities que contém duas funções, greet e capitalize. Podemos acessar essas funções usando o nome do namespace seguido do nome da função, fornecendo um agrupamento lógico para o código relacionado.

5. Recursos de programação orientada a objetos (OOP)

O TypeScript oferece suporte a conceitos de OOP, como classes, interfaces e herança, o que permite que você tenha um código estruturado e organizado.

Por exemplo:

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 avançado

O TypeScript oferece um sistema de tipos avançado que suporta genéricos, uniões, interseções e muito mais. Esses recursos aprimoram os recursos de verificação de tipos estáticos do TypeScript, permitindo que os desenvolvedores escrevam códigos mais robustos e expressivos.

Genéricos: Os genéricos permitem escrever código reutilizável que pode funcionar com diferentes tipos. Os genéricos são como espaços reservados para tipos determinados em tempo de execução com base nos valores passados para uma função ou uma classe.

Por exemplo, vamos definir uma função genérica identity que recebe um valor de argumento do tipo T e retorna um valor do mesmo 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

No exemplo acima, o tipo T é inferido com base no tipo de valor passado para a função. No primeiro uso da função identity, T é inferido como um número porque passamos 10 como argumento e, no segundo uso, T é inferido como uma string de caracteres porque passamos "hello" como argumento.

Uniões e interseções: As uniões e interseções são usadas para compor tipos e criar relações de tipos mais complexas.

As uniões permitem a combinação de dois ou mais tipos em um único tipo que pode ter qualquer um dos tipos combinados. As interseções permitem a combinação de dois ou mais tipos em um único tipo que deve satisfazer todos os tipos combinados.

Por exemplo, podemos definir dois tipos Employee e Manager, que representam um funcionário e um gerente, respectivamente.

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

Usando os tipos Employee e Manager, podemos definir um tipo de união EmployeeOrManager que pode ser um Employee ou um Manager.

type EmployeeOrManager = Employee | Manager; // Union type

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

No código acima, a variável person1 é do tipo EmployeeOrManager, o que significa que você pode atribuir a ela um objeto que atenda ao tipo Employee ou Manager.

Também podemos definir um tipo de interseção EmployeeOrManager que deve satisfazer os tipos Employee e Manager.

type EmployeeAndManager = Employee & Manager; // Intersection type

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

No código acima, a variável person2 é do tipo EmployeeAndManager, o que significa que ela deve ser um objeto que satisfaça os tipos Employee e Manager.

7. Compatibilidade com JavaScript

O TypeScript foi projetado para ser um superconjunto do JavaScript, o que significa que qualquer código JavaScript válido também é um código TypeScript válido. Isso facilita a integração do TypeScript em projetos JavaScript existentes sem que você precise reescrever todo o código.

O TypeScript é construído sobre o JavaScript, adicionando digitação estática opcional e recursos adicionais, mas ainda permite que você use o código JavaScript simples como está.

Por exemplo, se você tiver um arquivo JavaScript app.js, poderá renomeá-lo para app.ts e começar a usar os recursos do TypeScript gradualmente, sem alterar o código JavaScript existente. O TypeScript ainda será capaz de entender e compilar o código JavaScript como TypeScript válido.

Aqui está um exemplo de como o TypeScript fornece uma integração perfeita com o JavaScript:

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

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

Você pode renomear o arquivo JavaScript acima para app.ts e começar a usar os recursos do TypeScript:

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

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

No exemplo acima, adicionamos uma anotação de tipo ao parâmetro name, especificando como string, que é opcional no TypeScript. O restante do código permanece igual ao JavaScript. O TypeScript é capaz de entender o código JavaScript e fornecer verificação de tipo para a anotação de tipo adicionada, facilitando a adoção gradual do TypeScript em um projeto JavaScript existente.

Primeiros passos com o TypeScript

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

npm install -D typescript

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

Para o seu projeto JavaScript, você precisará primeiro inicializar um projeto node usando o seguinte comando para criar um arquivo package.json:

npm init -y

Em seguida, você pode instalar a dependência do TypeScript, criar arquivos TypeScript usando a extensão .ts e escrever seu código TypeScript.

Após escrever o código do TypeScript, você precisará compilar para JavaScript usando o compilador do TypeScript (tsc). Você pode executar o seguinte comando no diretório do projeto:

npx tsc .ts

Isso compila o código TypeScript no arquivo especificado para JavaScript e gera um arquivo .js com o mesmo nome.

Você pode então executar o código JavaScript compilado em seu projeto, da mesma forma que executaria um código JavaScript normal. Você pode usar o Node.js para executar o código JavaScript em um ambiente Node.js ou incluir o arquivo JavaScript compilado em um arquivo HTML e executá-lo em um navegador.

Trabalho com interfaces

As interfaces no TypeScript são usadas para definir contratos ou formas de objetos. Elas permitem que você especifique a estrutura ou a forma com a qual um objeto deve estar em conformidade.

As interfaces definem um conjunto de propriedades e/ou métodos que um objeto deve ter para ser considerado compatível com a interface. As interfaces podem ser usadas para fornecer anotações de tipo para objetos, parâmetros de função e valores de retorno, permitindo uma melhor verificação de tipo estático e sugestões de conclusão de código nos IDEs.

Aqui está um exemplo de uma interface no TypeScript:

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

Neste exemplo, definimos uma interface Person que especifica três propriedades: firstName do tipo string, lastName do tipo string e age do tipo number.

Qualquer objeto que tenha essas três propriedades com os tipos especificados será considerado compatível com a interface Person. Vamos agora definir os objetos que estão em conformidade com a interface Person:

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

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

Neste exemplo, criamos dois objetos person1 e person2 que estão em conformidade com a interface Person. Ambos os objetos têm as propriedades necessárias firstName, lastName e age com os tipos especificados, portanto são compatíveis com a interface Person.

Extensão de interfaces

As interfaces também podem ser estendidas para criar novas interfaces que herdam propriedades de interfaces existentes.

Por exemplo:

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

interface Dog extends Animal {
  breed: string;
}

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

Neste exemplo, definimos uma interface Animal com as propriedades name e sound e, em seguida, definimos uma nova interface “Dog” que estende a interface Animal e adiciona uma nova propriedadebreed. A interface Dog herda as propriedades da interface Animal, portanto, qualquer objeto que esteja em conformidade com a interface Dog também deve ter as propriedades name e sound.

Propriedades opcionais

As interfaces também podem ter propriedades opcionais, que são indicadas por um ? após o nome da propriedade.

Aqui está um exemplo:

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

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

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

Neste exemplo, definimos uma interface Car com as propriedades make e model, e uma propriedade opcional year. A propriedade year não é obrigatória, portanto, os objetos que estão em conformidade com a interface Car podem tê-la ou não.

Verificação avançada de tipos

O TypeScript também fornece opções avançadas para verificação de tipos no tsconfig.json. Essas opções podem aprimorar os recursos de verificação de tipo do seu projeto TypeScript e detectar possíveis erros no runtime, resultando em um código mais robusto e confiável.

1. strictNullChecks

Quando definido como true, o TypeScript impõe verificações nulas rigorosas, o que significa que as variáveis não podem ter um valor null ou undefined, a menos que sejam explicitamente especificadas com o tipo de união de null ou undefined.

Por exemplo:

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

Com essa opção ativada, o TypeScript detectará possíveis valores null ou undefined no runtime, ajudando a evitar erros de tempo de execução causados pelo acesso a propriedades ou métodos em variáveis ⁣, null ou 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

Quando definido como true, o TypeScript ativa a verificação rigorosa dos tipos de função, incluindo a bivariância de parâmetros de função, o que garante que os argumentos da função sejam rigorosamente verificados quanto à compatibilidade de tipos.

Por exemplo:

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

Com essa opção ativada, o TypeScript detectará possíveis incompatibilidades de tipo de parâmetro de função no runtime, ajudando a evitar erros de tempo de execução causados pela passagem de argumentos incorretos para funções.

// 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. noImplicitThis

Quando definido como true, o TypeScript não permite o uso de this com um tipo any implícito, ajudando a detectar possíveis erros ao usar this em métodos de classe.

Por exemplo:

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

Com essa opção ativada, o TypeScript detectará possíveis erros causados pelo uso de this sem anotações de tipo adequadas ou vinculação em métodos de classe.

// 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

A opção target especifica a versão de destino do ECMAScript para o seu código TypeScript. Ela determina a versão do JavaScript que o compilador do TypeScript deve gerar como saída.

Por exemplo:

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

Com essa opção definida como “ES2018“, o TypeScript irá gerar um código JavaScript que está em conformidade com o padrão ECMAScript 2018.

Isso pode ser útil se você quiser aproveitar os recursos e a sintaxe mais recentes do JavaScript, mas também precisar garantir a compatibilidade com ambientes JavaScript mais antigos.

5. module

A opção module especifica o sistema de módulos a ser usado em seu código TypeScript. As opções comuns incluem “CommonJS“, “AMD“, “ES6“, “ES2015“, etc. Isso determina como seus módulos TypeScript são compilados em módulos JavaScript.

Por exemplo:

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

Com essa opção definida como “ES6“, o TypeScript irá gerar um código JavaScript que usa a sintaxe do módulo ECMAScript 6.

Isso pode ser útil se você estiver trabalhando com um ambiente JavaScript moderno que ofereça suporte a módulos ECMAScript 6, como em um aplicativo de frontend usando um empacotador de módulos como o webpack ou o Rollup.

6. noUnusedLocals e noUnusedParameters

Essas opções permitem que o TypeScript capture variáveis locais não utilizadas e parâmetros de função, respectivamente.

Quando definido como true, o TypeScript emitirá erros de compilação para quaisquer variáveis locais ou parâmetros de função declarados, mas não usados no código.

Por exemplo:

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

Esses são apenas mais alguns exemplos de opções avançadas de verificação de tipo no arquivo tsconfig.json do TypeScript. Você pode consultar a documentação oficial para saber mais.

Práticas recomendadas e dicas para usar o TypeScript

1. Anote corretamente os tipos de variáveis, parâmetros de funções e valores de retorno

Um dos principais benefícios do TypeScript é seu forte sistema de digitação, que permite que você especifique explicitamente os tipos de variáveis, parâmetros de função e valores de retorno.

Isso melhora a legibilidade do código, detecta antecipadamente possíveis erros relacionados a tipos e permite o preenchimento inteligente de código nos IDEs.

Aqui está um exemplo:

// 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. Utilize os recursos avançados de tipo do TypeScript de forma eficaz

O TypeScript vem com um rico conjunto de recursos avançados de tipo, como genéricos, uniões, interseções, tipos condicionais e tipos mapeados. Esses recursos podem ajudar você a escrever códigos mais flexíveis e reutilizáveis.

Aqui está um exemplo:

// 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. Escreva código sustentável e escalável com o TypeScript

O TypeScript incentiva a escrita de código sustentável e escalável, fornecendo recursos como interfaces, classes e módulos.

Aqui está um exemplo:

// 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. Aproveite as ferramentas e suporte ao IDE do TypeScript

O TypeScript tem excelentes ferramentas e suporte para IDE, com recursos como preenchimento automático, inferência de tipos, refatoração e verificação de erros.

Aproveite esses recursos para aumentar sua produtividade e detectar possíveis erros no início do processo de desenvolvimento. Certifique-se de usar um IDE compatível com o TypeScript, como o Visual Studio Code, e instale o plugin do TypeScript para obter uma melhor experiência de edição de código.

Extensão TypeScript do VS Code
Extensão TypeScript do VS Code

Resumo

O TypeScript oferece uma ampla gama de recursos avançados que podem melhorar muito seus projetos de desenvolvimento da web.

Sua forte digitação estática, seu sistema de tipos avançado e seus recursos de programação orientada a objetos fazem dele uma ferramenta valiosa para escrever códigos robustos, escalonáveis e de fácil manutenção. As ferramentas e o suporte ao IDE do TypeScript também proporcionam uma experiência de desenvolvimento perfeita.

Se quiser explorar o TypeScript e seus recursos, você pode fazer isso hoje mesmo, graças à hospedagem de aplicativos da Kinsta.