A programação assíncrona é um paradigma de programação que permite escrever código que é executado assíncronamente
. Em contraste com a programação síncrona, que executa o código de forma sequencial, a programação assíncrona permite que o código seja executado em segundo plano enquanto o restante do programa continua a ser executado. Isso é particularmente útil para tarefas que podem levar muito tempo para serem concluídas, como buscar dados de uma API remota.
Programação assíncrona
é essencial para criar aplicações responsivas e eficientes em JavaScript. TypeScript, um superconjunto de JavaScript, torna ainda mais fácil trabalhar com programação assíncrona.
Existem várias abordagens para programação assíncrona
em TypeScript, incluindo o uso de promessas
, async/await
e callbacks
. Vamos abordar cada uma dessas abordagens em detalhes para que você possa escolher a melhor (ou as melhores) para o seu caso de uso.
Índice
Por que a Programação Assíncrona é Importante?
A programação assíncrona é crucial para a construção de aplicações web responsivas e eficientes. Ela permite que tarefas sejam executadas em segundo plano enquanto o restante do programa continua, mantendo a interface do usuário responsiva a entradas. Além disso, a programação assíncrona pode melhorar o desempenho geral ao permitir que várias tarefas sejam executadas ao mesmo tempo.
Há muitos exemplos do mundo real de programação assíncrona, como acessar câmeras e microfones de usuários e lidar com eventos de entrada do usuário. Mesmo que você não crie frequentemente funções assíncronas, é importante saber como usá-las corretamente para garantir que sua aplicação seja confiável e tenha um bom desempenho.
Como o TypeScript Facilita a Programação Assíncrona
O TypeScript oferece várias funcionalidades que simplificam a programação assíncrona, incluindo segurança de tipos
, inferência de tipos
, verificação de tipos
e anotações de tipos
.
Com a segurança de tipos, você pode garantir que seu código se comporte como esperado, mesmo ao lidar com funções assíncronas. Por exemplo, o TypeScript pode identificar erros relacionados a valores nulos e indefinidos em tempo de compilação, economizando tempo e esforço na depuração.
A inferência e verificação de tipos do TypeScript também reduzem a quantidade de código repetitivo que você precisa escrever, tornando seu código mais conciso e fácil de ler.
E as anotações de tipos do TypeScript fornecem clareza e documentação para seu código, o que é especialmente útil ao trabalhar com funções assíncronas que podem ser complexas de entender.
Agora vamos mergulhar e aprender sobre essas três características-chave da programação assíncrona: promessas, async/await e callbacks.
Como Usar Promessas no TypeScript
As promessas são uma ferramenta poderosa para lidar com operações assíncronas no TypeScript. Por exemplo, você pode usar uma promessa para buscar dados de uma API externa ou para realizar uma tarefa demorada em segundo plano enquanto sua thread principal continua em execução.
Para usar uma promessa, você cria uma nova instância da classe Promise
e passa a ela uma função que realiza a operação assíncrona. Esta função deve chamar o método resolve com o resultado quando a operação for bem-sucedida ou o método reject com um erro se falhar.
Uma vez que a Promessa é criada, você pode anexar callbacks a ela usando o método then
. Esses callbacks serão acionados quando a Promessa for cumprida, com o valor resolvido passado como parâmetro. Se a Promessa for rejeitada, você pode anexar um manipulador de erro usando o método catch, que será chamado com o motivo da rejeição.
O uso de Promessas oferece várias vantagens sobre métodos baseados em callbacks tradicionais. Por exemplo, as Promessas podem ajudar a evitar o “inferno de callbacks”, um problema comum em código assíncrono onde callbacks aninhados se tornam difíceis de ler e manter.
As Promessas também facilitam o tratamento de erros em código assíncrono, pois você pode usar o método catch para gerenciar erros que ocorrem em qualquer lugar na cadeia de Promessas.
Por fim, as Promessas podem simplificar seu código, fornecendo uma maneira consistente e componível de lidar com operações assíncronas, independentemente de sua implementação subjacente.
Como Criar uma Promise
Sintaxe da Promise:
const myPromise = new Promise((resolve, reject) => {
// Realize alguma operação assíncrona
// Se a operação for bem-sucedida, chame o resolve com o resultado
// Se a operação falhar, chame o reject com um objeto de erro
});
myPromise
.then((result) => {
// Lidar com o resultado bem-sucedido
})
.catch((error) => {
// Lidar com o erro
});
// Exemplo 1 de como criar uma promise
function myAsyncFunction(): Promise<string> {
return new Promise<string>((resolve, reject) => {
// Alguma operação assíncrona
setTimeout(() => {
// Operação bem-sucedida resolve a promiseConfira minha última postagem no blog sobre dominar programação assíncrona em TypeScript! Aprenda a trabalhar com Promises, Async/Await e Callbacks para escrever código eficiente e escalável. Prepare-se para levar suas habilidades em TypeScript para o próximo nível!
const success = true;
if (success) {
// Resolver a promise com o resultado da operação se a operação foi bem-sucedida
resolve(
`The result is success and your operation result is ${operationResult}`
);
} else {
const rejectCode: number = 404;
const rejectMessage: string = `The result is failed and your operation result is ${rejectCode}`;
// Rejeitar a promise com o resultado da operação se a operação falhou
reject(new Error(rejectMessage));
}
}, 2000);
});
}
// Usar a promise
myAsyncFunction()
.then((result) => {
console.log(result); // saída: O resultado é sucesso e o resultado da sua operação é 4
})
.catch((error) => {
console.error(error); // saída: O resultado é falhou e o resultado da sua operação é 404
});
No exemplo acima, temos uma função chamada myAsyncFunction()
que retorna uma promise
. Usamos o construtor Promise
para criar a promise, o qual recebe uma função de retorno de chamada
com os argumentos resolve
e reject
. Se a operação assíncrona for bem-sucedida, chamamos a função resolve. Se falhar, chamamos a função reject.
O objeto promise retornado pelo construtor possui um método then()
, o qual recebe funções de retorno de chamada para sucesso e falha. Se a promise for resolvida com sucesso, a função de retorno de chamada de sucesso é chamada com o resultado. Se a promise for rejeitada, a função de retorno de chamada de falha é chamada com uma mensagem de erro.
O objeto promise também possui um método catch()
usado para lidar com erros que ocorrem durante a cadeia de promises. O método catch()
recebe uma função de retorno de chamada, a qual é chamada se ocorrer algum erro na cadeia de promises.
Agora, vamos avançar para como encadear promises no TypeScript.
Como Encadear Promises
Encadear promises permite que você execute múltiplas operações assíncronas
em sequência ou em paralelo. Isso é útil quando você precisa realizar várias tarefas assíncronas uma após a outra ou ao mesmo tempo. Por exemplo, você pode precisar buscar dados de forma assíncrona e processá-los de forma assíncrona.
Vamos ver um exemplo de como encadear promises:
// Exemplo de como funciona o encadeamento de promessas
// Primeira promessa
const promise1 = new Promise((resolve, reject) => {
const functionOne: string = "This is the first promise function";
setTimeout(() => {
resolve(functionOne);
}, 1000);
});
// Segunda promessa
const promise2 = (data: number) => {
const functionTwo: string = "This is the second second promise function";
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(` ${data} '+' ${functionTwo} `);
}, 1000);
});
};
// Encadeando a primeira e a segunda promessas juntas
promise1
.then(promise2)
.then((result) => {
console.log(result); // saída: Esta é a função da primeira promessa + Esta é a função da segunda promessa
})
.catch((error) => {
console.error(error);
});
No exemplo acima, temos duas promessas: promessa1
e promessa2
. promessa1
é resolvida após 1 segundo com a string “Esta é a função da primeira promessa.” promessa2
recebe um número como entrada e retorna uma promessa que é resolvida após 1 segundo com uma string que combina o número de entrada e a string “Esta é a função da segunda promessa.”
Encadeamos as duas promessas usando o método then
. A saída da promessa1
é passada como entrada para a promessa2
. Por fim, usamos novamente o método then
para exibir no console a saída da promessa2
. Se qualquer uma das promessas promessa1
ou promessa2
for rejeitada, o erro será capturado pelo método catch
.
Parabéns! Você aprendeu como criar e encadear promessas no TypeScript. Agora você pode usar promessas para realizar operações assíncronas no TypeScript. Agora, vamos explorar como o Async/Await
funciona no TypeScript.
Como Usar Async / Await no TypeScript
Async/await é uma sintaxe introduzida no ES2017 para facilitar o trabalho com Promises. Permite escrever código assíncrono que parece e se sente como código síncrono.
No TypeScript, você pode definir uma função assíncrona usando a palavra-chave async
. Isso informa ao compilador que a função é assíncrona e retornará uma Promise.
Agora, vamos ver como usar async/await no TypeScript.
Sintaxe Async / Await:
// Sintaxe Async / Await no TypeScript
async function functionName(): Promise<ReturnType> {
try {
const result = await promise;
// código a ser executado após a resolução da promise
return result;
} catch (error) {
// código a ser executado se a promise for rejeitada
throw error;
}
}
No exemplo acima, functionName
é uma função async que retorna uma Promise de ReturnType
. A palavra-chave await
é usada para aguardar a resolução da promise antes de passar para a próxima linha de código.
O bloco try/catch
é usado para lidar com quaisquer erros que ocorram durante a execução do código dentro da função async. Se um erro ocorrer, ele será capturado pelo bloco catch, onde você pode tratá-lo adequadamente.
Usando Arrow Functions com Async / Await
Você também pode usar funções de seta com a sintaxe async/await no TypeScript:
const functionName = async (): Promise<ReturnType> => {
try {
const result = await promise;
// código a ser executado após a resolução da promise
return result;
} catch (error) {
// código a ser executado se a promise for rejeitada
throw error;
}
};
No exemplo acima, functionName
é definido como uma função de seta que retorna uma Promise de ReturnType
. A palavra-chave async indica que esta é uma função assíncrona, e a palavra-chave await é usada para aguardar a resolução da promise antes de passar para a próxima linha de código.
Async / Await com uma Chamada de API
Agora, vamos além da sintaxe e buscar alguns dados de uma API usando async/await.
interface User {
id: number;
name: string;
email: string;
}
const fetchApi = async (): Promise<void> => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(
`Failed to fetch users (HTTP status code: ${response.status})`
);
}
const data: User[] = await response.json();
console.log(data);
} catch (error) {
console.error(error);
throw error;
}
};
fetchApi();
Aqui, estamos buscando dados da API JSONPlaceholder, convertendo-os em JSON e então exibindo no console. Este é um exemplo do mundo real de como usar async/await no TypeScript.
Você deverá ver informações do usuário no console. Esta imagem mostra a saída:
Async/Await com Chamada de API do Axios
// Exemplo 2 de como usar async/await em typescript
const fetchApi = async (): Promise<void> => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
const data = await response.data;
console.log(data);
} catch (error) {
console.error(error);
}
};
fetchApi();
No exemplo acima, definimos a função fetchApi()
usando async/await e o método Axios.get()
para fazer uma solicitação HTTP GET para a URL especificada. Usamos await para aguardar a resposta, então extraímos os dados usando a propriedade data do objeto de resposta. Por fim, registramos os dados no console com console.log()
. Quaisquer erros que ocorrerem serão capturados e registrados no console com console.error()
.
Podemos alcançar isso usando o Axios, então você deverá ver o mesmo resultado no console.
Esta imagem mostra a saída ao usar o Axios no console:
Nota: Antes de tentar o código acima, você precisa instalar o Axios usando npm ou yarn.
npm install axios
yarn add axios
Se você não está familiarizado com o Axios, você pode aprender mais sobre ele aqui.
Você pode ver que usamos um bloco try
e catch
para lidar com erros. O bloco try
e catch
é um método para gerenciar erros no TypeScript. Portanto, sempre que fizer chamadas de API como acabamos de fazer, certifique-se de usar um bloco try
e catch
para lidar com quaisquer erros.
Agora, vamos explorar um uso mais avançado do bloco try
e catch
no TypeScript:
// Exemplo 3 de como usar async/await no typescript
interface Recipe {
id: number;
name: string;
ingredients: string[];
instructions: string[];
prepTimeMinutes: number;
cookTimeMinutes: number;
servings: number;
difficulty: string;
cuisine: string;
caloriesPerServing: number;
tags: string[];
userId: number;
image: string;
rating: number;
reviewCount: number;
mealType: string[];
}
const fetchRecipes = async (): Promise<Recipe[] | string> => {
const api = "https://dummyjson.com/recipes";
try {
const response = await fetch(api);
if (!response.ok) {
throw new Error(`Failed to fetch recipes: ${response.statusText}`);
}
const { recipes } = await response.json();
return recipes; // Retorna o array de receitas
} catch (error) {
console.error("Error fetching recipes:", error);
if (error instanceof Error) {
return error.message;
}
return "An unknown error occurred.";
}
};
// Busca e exibe receitas
fetchRecipes().then((data) => {
if (Array.isArray(data)) {
console.log("Recipes fetched successfully:", data);
} else {
console.error("Error message:", data);
}
});
No exemplo acima, definimos uma interface Recipe
que delineia a estrutura dos dados que esperamos da API. Em seguida, criamos a função fetchRecipes()
usando async/await e o método fetch() para fazer uma solicitação HTTP GET para o endpoint da API especificado.
Usamos um bloco try/catch
para lidar com quaisquer erros que possam ocorrer durante a solicitação da API. Se a solicitação for bem-sucedida, extraímos a propriedade de dados da resposta usando await e a retornamos. Se ocorrer um erro, verificamos se há uma mensagem de erro e a retornamos como uma string, se existir.
Finalmente, chamamos a função fetchRecipes()
e utilizamos .then()
para registrar os dados retornados no console. Este exemplo demonstra como usar async/await
com blocos try/catch
para lidar com erros em um cenário mais avançado, onde precisamos extrair dados de um objeto de resposta e retornar uma mensagem de erro personalizada.
Esta imagem mostra o resultado de saída do código:
Async / Await com Promise.all
Promise.all()
é um método que recebe um array de promessas como entrada (um iterável) e retorna uma única Promessa como saída. Esta Promessa é resolvida quando todas as promessas de entrada foram resolvidas ou se o iterável de entrada não contiver promessas. Ela é rejeitada imediatamente se alguma das promessas de entrada for rejeitada ou se não-promessas lançarem um erro, e será rejeitada com a primeira mensagem de rejeição ou erro.
// Exemplo de uso de async / await com Promise.all
interface User {
id: number;
name: string;
email: string;
profilePicture: string;
}
interface Post {
id: number;
title: string;
body: string;
}
interface Comment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
const fetchApi = async <T>(url: string): Promise<T> => {
try {
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Network response was not ok for ${url}`);
}
} catch (error) {
console.error(error);
throw new Error(`Error fetching data from ${url}`);
}
};
const fetchAllApis = async (): Promise<[User[], Post[], Comment[]]> => {
try {
const [users, posts, comments] = await Promise.all([
fetchApi<User[]>("https://jsonplaceholder.typicode.com/users"),
fetchApi<Post[]>("https://jsonplaceholder.typicode.com/posts"),
fetchApi<Comment[]>("https://jsonplaceholder.typicode.com/comments"),
]);
return [users, posts, comments];
} catch (error) {
console.error(error);
throw new Error("Error fetching data from one or more APIs");
}
};
fetchAllApis()
.then(([users, posts, comments]) => {
console.log("Users: ", users);
console.log("Posts: ", posts);
console.log("Comments: ", comments);
})
.catch((error) => console.error(error));
No código acima, usamos Promise.all
para buscar várias APIs ao mesmo tempo. Se você tiver várias APIs para buscar, pode usar Promise.all
para obtê-las todas de uma vez. Como pode ver, usamos map
para percorrer o array de APIs e depois passá-lo para Promise.all
para buscá-las simultaneamente.
A imagem abaixo mostra a saída das chamadas da API:
Vamos ver como usar Promise.all
com o Axios:
// Exemplo de uso de async / await com axios e Promise.all
const fetchApi = async () => {
try {
const urls = [
"https://jsonplaceholder.typicode.com/users",
"https://jsonplaceholder.typicode.com/posts",
];
const responses = await Promise.all(urls.map((url) => axios.get(url)));
const data = await Promise.all(responses.map((response) => response.data));
console.log(data);
} catch (error) {
console.error(error);
}
};
fetchApi();
No exemplo acima, estamos usando Promise.all
para buscar dados de duas URLs diferentes ao mesmo tempo. Primeiro, criamos um array de URLs, em seguida, usamos o map para criar um array de Promises a partir das chamadas de axios.get
. Passamos este array para Promise.all
, que retorna um array de respostas. Por fim, usamos o map novamente para obter os dados de cada resposta e registrá-los no console.
Como Usar Callbacks no TypeScript
Um callback é uma função passada como argumento para outra função. A função de callback é executada dentro da outra função. Callbacks garantem que uma função não seja executada antes que uma tarefa seja concluída – mas que ela seja executada imediatamente após a conclusão da tarefa. Eles nos ajudam a escrever código JavaScript assíncrono e evitar problemas e erros.
// Exemplo de uso de callbacks no TypeScript
const add = (a: number, b: number, callback: (result: number) => void) => {
const result = a + b;
callback(result);
};
add(10, 20, (result) => {
console.log(result);
});
A imagem abaixo mostra a função de callback:
Vamos ver outro exemplo de uso de callbacks no TypeScript:
// Exemplo de uso de uma função de callback no TypeScript
type User = {
name: string;
email: string;
};
const fetchUserData = (
id: number,
callback: (error: Error | null, user: User | null) => void
) => {
const api = `https://jsonplaceholder.typicode.com/users/${id}`;
fetch(api)
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error("Network response was not ok.");
}
})
.then((data) => {
const user: User = {
name: data.name,
email: data.email,
};
callback(null, user);
})
.catch((error) => {
callback(error, null);
});
};
// Uso de fetchUserData com uma função de callback
fetchUserData(1, (error, user) => {
if (error) {
console.error(error);
} else {
console.log(user);
}
});
No exemplo acima, temos uma função chamada fetchUserData
que recebe um id
e um callback
como parâmetros. Este callback
é uma função com dois parâmetros: um erro e um usuário.
A função fetchUserData
recupera dados do usuário de um ponto de extremidade da API JSONPlaceholder usando o id
. Se a recuperação for bem-sucedida, ela cria um objeto User
e o passa para a função de retorno de chamada com um erro nulo. Se houver um erro durante a recuperação, ele envia o erro para a função de retorno de chamada com um usuário nulo.
Para usar a função fetchUserData
com um retorno de chamada, fornecemos um id
e uma função de retorno de chamada como argumentos. A função de retorno de chamada verifica erros e registra os dados do usuário se não houver erros.
A imagem abaixo mostra a saída das chamadas da API:
Como Usar Retornos de Chamada de Forma Responsável
Embora os retornos de chamada sejam fundamentais para a programação assíncrona em TypeScript, eles exigem gerenciamento cuidadoso para evitar a “callback hell” – o código profundamente aninhado em forma de pirâmide que se torna difícil de ler e manter. Veja como usar retornos de chamada de forma eficaz:
-
Avoid deep nesting
-
Desmembre a estrutura do seu código dividindo operações complexas em funções nomeadas
-
Use promises ou async/await para fluxos de trabalho assíncronos complexos (mais sobre isso abaixo)
-
-
Tratamento de erros primeiro
-
Siga sempre a convenção do Node.js de parâmetros
(erro, resultado)
-
Verifique erros em todos os níveis de callbacks aninhados
-
function processData(input: string, callback: (err: Error | null, result?: string) => void) {
// ... sempre chame o callback com o erro primeiro
}
-
Use anotações de tipo
-
Aproveite o sistema de tipos do TypeScript para impor assinaturas de callbacks
-
Defina interfaces claras para os parâmetros de callback
-
type ApiCallback = (error: Error | null, data?: ApiResponse) => void;
-
Considere bibliotecas de fluxo de controle
Para operações assíncronas complexas, utilize utilitários comoasync.js
para:-
Execução paralela
-
Execução em série
-
Tratamento de pipelines de erros
-
Quando usar callbacks vs. alternativas
Há momentos em que callbacks são uma ótima escolha, e outros em que não são.
Callbacks são úteis quando você está trabalhando com operações assíncronas (conclusão única), interagindo com bibliotecas ou APIs mais antigas que esperam callbacks, lidando com ouvintes de eventos (como ouvintes de clique ou eventos de websocket) ou criando utilitários leves com necessidades assíncronas simples.
Em outros cenários onde você precisa se concentrar em escrever código manutenível com um fluxo assíncrono claro, callbacks causam problemas e você deve preferir promessas ou async-await. Por exemplo, quando você precisa encadear várias operações, lidar com complexa propagação de erros, trabalhar com APIs modernas (como a Fetch API ou FS Promises), ou usar promise.all()
para execução paralela.
Exemplo de migração de callbacks para promessas:
// Versão de retorno de chamada
function fetchUser(id: number, callback: (err: Error | null, user?: User) => void) {
// ...
}
// Versão de Promessa
async function fetchUserAsync(id: number): Promise<User> {
// ...
}
// Uso com async/await
try {
const user = await fetchUserAsync(1);
} catch (error) {
// Lidar com erro
}
A Evolução dos Padrões Assíncronos
Padrão | Prós | Contras |
Retornos de Chamada | Simples, universal | Complexidade aninhada |
Promessas | Encadeáveis, melhor fluxo de erro | Requer correntes .then() |
Async/Await | Legibilidade semelhante a sincronia | Requer transpilação |
Projetos modernos do TypeScript frequentemente usam uma combinação: retornos de chamada para padrões orientados a eventos e promessas/async-await para lógica assíncrona complexa. A chave é escolher a ferramenta certa para o seu caso de uso específico, mantendo a clareza do código.
Conclusão
Neste artigo, aprendemos sobre as diferentes maneiras de lidar com código assíncrono em TypeScript. Aprendemos sobre retornos de chamada, promessas, async/await e como usá-los em TypeScript. Também aprendemos sobre esse conceito.
Se você quiser aprender mais sobre programação e como se tornar um melhor engenheiro de software, você pode se inscrever no meu canal do YouTube CliffTech.
Obrigado por ler o meu artigo. Espero que tenha gostado. Se tiver alguma dúvida, sinta-se à vontade para entrar em contato comigo.
Conecte-se comigo nas redes sociais: