TypeScript e Promises: Lidando com Operações Assíncronas

18/09/2024

Por Que Usar TypeScript com Promises?

Promises são uma maneira popular de lidar com operações assíncronas no JavaScript, como requisições HTTP, leitura de arquivos e temporizadores. Ao usar TypeScript com Promises, você pode garantir que os valores retornados e os erros tratados sejam fortemente tipados, aumentando a segurança do código. O TypeScript ajuda a evitar erros comuns, como retornos inesperados ou tipos incorretos, fornecendo feedback imediato durante o desenvolvimento.

O Que é uma Promise?

Uma Promise é um objeto que representa a eventual conclusão (ou falha) de uma operação assíncrona e seu valor resultante. Uma Promise pode estar em três estados: pending (pendente), fulfilled (concluída) ou rejected (rejeitada).

Exemplo básico de uma Promise:

const minhaPromise = new Promise((resolve, reject) => {
  const sucesso = true;
  
  if (sucesso) {
    resolve('Operação bem-sucedida!');
  } else {
    reject('Ocorreu um erro.');
  }
});

minhaPromise
  .then((resultado) => console.log(resultado))
  .catch((erro) => console.log(erro));

Aqui, a Promise chama resolve se a operação for bem-sucedida, ou reject se houver um erro. O TypeScript, ao combinar tipagem com Promises, garante que os tipos retornados e erros tratados sejam coerentes.

Criando e Tipando Promises no TypeScript

Ao trabalhar com Promises no TypeScript, é essencial definir o tipo do valor que será retornado quando a Promise for resolvida. Isso garante que o tipo de dado retornado seja corretamente inferido em todas as partes do código.

1. Promises com Tipagem Básica

Veja como criar e tipar uma Promise que retorna um valor numérico:

function obterNumero(): Promise<number> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(42);
    }, 1000);
  });
}

obterNumero()
  .then((numero) => console.log(`Número recebido: ${numero}`))
  .catch((erro) => console.log(`Erro: ${erro}`));

Aqui, a função obterNumero retorna uma Promise<number>, o que garante que a função assíncrona sempre retornará um número. O TypeScript verificará o tipo no then, garantindo que o numero seja tratado corretamente como number.

2. Promises com Tipagem de Erros

Também podemos garantir que os erros tratados em uma Promise sejam de tipos específicos. Veja um exemplo de como tipar o valor de erro:

function buscarUsuario(id: number): Promise<string> {
  return new Promise((resolve, reject) => {
    if (id === 1) {
      resolve('Usuário encontrado: Mauro Souza');
    } else {
      reject(new Error('Usuário não encontrado.'));
    }
  });
}

buscarUsuario(2)
  .then((usuario) => console.log(usuario))
  .catch((erro: Error) => console.log(erro.message));

Aqui, o catch está tipado para garantir que o erro seja uma instância de Error, o que permite acessar a propriedade message de forma segura.

Funções Assíncronas com async/await no TypeScript

O TypeScript oferece excelente suporte para funções assíncronas usando async e await. Com await, você pode escrever código assíncrono de forma mais linear, sem a necessidade de encadeamentos complexos de then e catch.

1. Usando async/await com Tipagem

Veja como criar uma função assíncrona que retorna uma Promise tipada no TypeScript:

async function obterDados(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => resolve('Dados recebidos!'), 2000);
  });
}

async function processarDados() {
  try {
    const dados = await obterDados();
    console.log(dados);
  } catch (erro) {
    console.error(erro);
  }
}

processarDados();

Nesse exemplo, a função obterDados retorna uma Promise<string>, e a função processarDados usa await para esperar a resolução dessa Promise. O TypeScript garante que o valor retornado seja do tipo string e que o catch trate o erro corretamente.

2. Encadeando Funções Assíncronas

Você pode encadear múltiplas funções assíncronas e garantir que todos os valores estejam corretamente tipados:

async function primeiraOperacao(): Promise<number> {
  return 10;
}

async function segundaOperacao(valor: number): Promise<string> {
  return `Resultado: ${valor * 2}`;
}

async function executarOperacoes() {
  try {
    const numero = await primeiraOperacao();
    const resultado = await segundaOperacao(numero);
    console.log(resultado);
  } catch (erro) {
    console.error(erro);
  }
}

executarOperacoes();

Neste exemplo, o TypeScript garante que a função segundaOperacao receba o valor numérico correto de primeiraOperacao e retorne uma string tipada. A função executarOperacoes lida com a execução sequencial dessas operações assíncronas.

Tratamento de Erros em Promises com TypeScript

O tratamento de erros é uma parte importante ao trabalhar com Promises. O TypeScript permite tipar os erros e garantir que os tipos de erro sejam tratados adequadamente. Isso é especialmente útil ao trabalhar com APIs que podem retornar diferentes tipos de erro.

1. Tratando Erros com try/catch

Em uma função assíncrona, o try/catch é uma maneira comum de tratar erros. Veja como isso funciona com TypeScript:

async function buscarDados(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Erro ao buscar dados')), 1000);
  });
}

async function processar() {
  try {
    const dados = await buscarDados();
    console.log(dados);
  } catch (erro: any) {
    if (erro instanceof Error) {
      console.error(erro.message);
    }
  }
}

processar();

No catch, o TypeScript permite que você verifique se o erro é uma instância de Error antes de acessar suas propriedades, garantindo segurança ao manipular o erro.

2. Tratamento Global de Erros

Em projetos maiores, pode ser útil criar uma função global para tratar erros de Promises, garantindo consistência na manipulação de erros:

function tratarErro(erro: unknown) {
  if (erro instanceof Error) {
    console.error('Erro:', erro.message);
  } else {
    console.error('Erro desconhecido:', erro);
  }
}

async function executarComErro() {
  try {
    throw new Error('Erro simulado!');
  } catch (erro) {
    tratarErro(erro);
  }
}

executarComErro();

Neste exemplo, a função tratarErro lida com erros de forma genérica, garantindo que qualquer erro seja tratado adequadamente, independentemente do tipo.

Boas Práticas ao Usar Promises com TypeScript

  • Defina Tipos de Retorno para Promises: Sempre que possível, defina explicitamente o tipo de retorno das Promises para garantir a previsibilidade do código.
  • Use async/await para Fluxos Mais Claros: Prefira async/await para operações assíncronas complexas, pois melhora a legibilidade do código em comparação ao encadeamento de then e catch.
  • Verifique o Tipo de Erros: Use instanceof para garantir que os erros capturados sejam do tipo correto antes de manipulá-los.
  • Evite o Uso de any: Tipar erros como any deve ser evitado. Use unknown para representar erros desconhecidos e verifique seus tipos adequadamente.
  • Trate Erros de Forma Global: Considere criar funções globais para lidar com erros, garantindo consistência em toda a aplicação.

Conclusão

Combinar Promises com TypeScript oferece uma maneira segura e eficiente de lidar com operações assíncronas. A tipagem forte garante que os valores retornados sejam previsíveis e que os erros sejam tratados corretamente. Usando async/await e tipando adequadamente as Promises, você pode melhorar a legibilidade do código e evitar erros em tempo de execução, tornando o desenvolvimento mais confiável e produtivo.