Tipagem para funções async e Promise em TypeScript
1. Visão geral
Funções async estão presentes na maioria do código TypeScript moderno: chamadas de API, leitura de banco de dados, processamento de filas. Escrever uma função async é fácil. Declarar tipos para ela de forma que o compilador capture erros é mais difícil.
Este artigo apresenta como declarar tipos para funções async e Promise. Os conteúdos principais incluem Promise<T>, Awaited<T>, tipos de Promise.all e Promise.allSettled, como tratar erros com segurança de tipo, e helpers genéricos como timeout. Após ler, você evitará erros sutis como esquecer await ou usar async dentro de forEach.
2. Preparando o ambiente
Os exemplos usam TypeScript em modo strict e Vitest para testes:
npm init -y
npm install -D typescript vitest
npx tsc --init --strict --target es2022 --module nodenext
A flag --strict ativa useUnknownInCatchVariables. Esta flag é importante para a seção de tratamento de erros em 5.
Todos os exemplos usam um modelo simples e uma função que simula latência de rede:
// models.ts
export interface Order {
id: string;
customerId: string;
total: number;
status: 'PENDING' | 'PAID';
}
export interface Customer {
id: string;
name: string;
}
export const delay = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
delay retorna Promise<void>. Este é o tipo de uma Promise que apenas indica “concluído” sem carregar um valor. Aqui você deve especificar new Promise<void>. Se não especificar, TypeScript infere Promise<unknown>.
3. Tipo de retorno de funções async
3.1. Sempre é Promise<T>
Uma função async sempre retorna uma Promise. O valor que você return dentro dela será encapsulado nela:
// order-service.ts
import { Order, Customer, delay } from './models';
const orders = new Map<string, Order>([
['o-1', { id: 'o-1', customerId: 'c-1', total: 250_000, status: 'PENDING' }],
]);
export async function fetchOrder(id: string): Promise<Order> {
await delay(50);
const order = orders.get(id);
if (!order) {
throw new Error(`Pedido ${id} não encontrado`);
}
return order; // tipo Order, encapsulado em Promise<Order>
}
export async function fetchCustomer(id: string): Promise<Customer> {
await delay(30);
return { id, name: 'Nguyễn Văn An' };
}
O tipo declarado é Promise<Order>, não Order. Se você escrever async function fetchOrder(id: string): Order, o compilador reporta um erro:
erro TS1064: O tipo de retorno de uma função async ou método deve ser o tipo global Promise<T>.
O comando throw dentro de uma função async não muda o tipo de retorno. O erro é convertido em uma Promise rejeitada. Quem chama a função captura usando try/catch em volta de await, ou usando .catch().
3.2. Inferência ou declaração explícita?
TypeScript infere automaticamente o tipo de retorno se você omitir a anotação. Mesmo assim, para funções públicas você deve especificar o tipo. Veja o exemplo a seguir:
async function findOrder(id: string) {
const order = orders.get(id);
if (!order) return null;
return order;
}
// Tipo inferido: Promise<Order | null>
Este tipo está correto, mas depende do corpo da função. Suponha que alguém modifique o primeiro ramo para retornar undefined. O tipo silenciosamente muda para Promise<Order | undefined>, e todos os locais que verificam === null estarão errados sem aviso. Ao especificar Promise<Order | null> explicitamente, o compilador reporta erro imediatamente na função.
4. Obtendo o tipo “dentro” de Promise com Awaited<T>
Às vezes você tem uma função async e precisa do tipo do valor após await. ReturnType não é suficiente, pois retorna a própria Promise:
import { fetchOrder } from './order-service';
type R1 = ReturnType<typeof fetchOrder>; // Promise<Order>
type R2 = Awaited<ReturnType<typeof fetchOrder>>; // Order
type Nested = Awaited<Promise<Promise<number>>>; // number
Awaited<T> simula corretamente o comportamento de await. Ele “desembrulha” Promise recursivamente, inclusive quando Promises estão aninhadas. Este tipo é muito útil ao escrever genéricos, ou quando uma biblioteca não exporta o tipo do resultado:
type OrderSummary = Pick<Awaited<ReturnType<typeof fetchOrder>>, 'id' | 'total'>;
// { id: string; total: number }
5. Tipagem para erros: Promise não tem parâmetro de erro
Promise<T> tem apenas um parâmetro de tipo, reservado para o valor em caso de sucesso. Não há sintaxe Promise<Order, NotFoundError>. Portanto, a variável em catch tem tipo unknown:
async function printOrder(id: string): Promise<void> {
try {
const order = await fetchOrder(id);
console.log(order.total);
} catch (err) {
// err: unknown
if (err instanceof Error) {
console.error('Erro:', err.message);
} else {
console.error('Erro desconhecido:', err);
}
}
}
await printOrder('o-404');
Erro: Pedido o-404 não encontrado
JavaScript permite throw de qualquer valor: string, número, objeto. Portanto, unknown é o único tipo honesto para err. Você deve estreitar o tipo usando instanceof antes de acessar err.message.
Se você quiser que o erro se torne parte do tipo, pode usar o padrão Result:
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function attempt<T>(promise: Promise<T>): Promise<Result<T>> {
try {
return { ok: true, value: await promise };
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
return { ok: false, error };
}
}
const result = await attempt(fetchOrder('o-1'));
if (result.ok) {
console.log(result.value.status); // result.value: Order
} else {
console.log(result.error.message);
}
PENDING
A propriedade ok é um discriminante. Após verificar if (result.ok), TypeScript tem certeza de que result.value existe e tem tipo Order. Quem chama a função não pode “esquecer” de tratar o erro, pois para obter value deve verificar ok primeiro.
Nota: Não encapsule toda função com
Result. Este padrão funciona bem para erros de negócio esperados, como “não encontrado” ou “fora de estoque”. Para erros de infraestrutura como perda de conexão com BD, deixe a exceção propagar até a camada de tratamento centralizado.
6. Tipos de Promise.all, allSettled e race
6.1. Promise.all preserva tipo de tupla
Ao passar um array literal, TypeScript infere tipos para cada posição:
const [order, customer] = await Promise.all([
fetchOrder('o-1'),
fetchCustomer('c-1'),
]);
// order: Order, customer: Customer
console.log(`${customer.name} - ${order.total}`);
Nguyễn Văn An - 250000
As duas requisições rodam em paralelo. O tempo total é cerca de 50ms, não 80ms. O tipo do resultado é [Order, Customer], não (Order | Customer)[]. Assim você pode fazer destructuring sem precisar de casting.
Isto só é verdade quando você passa o array diretamente. Se criar o array antes e depois passá-lo, o tipo de tupla é perdido:
const tasks = [fetchOrder('o-1'), fetchCustomer('c-1')];
const results = await Promise.all(tasks);
// results: (Order | Customer)[] -- informação de posição perdida
const tasksTuple = [fetchOrder('o-1'), fetchCustomer('c-1')] as const;
const [o, c] = await Promise.all(tasksTuple); // Order, Customer
Adicionar as const ajuda a preservar o tipo de tupla.
6.2. Promise.allSettled e estreitamento de tipo
Promise.allSettled nunca rejeita. Cada elemento no resultado tem tipo PromiseSettledResult<T>:
const settled = await Promise.allSettled([
fetchOrder('o-1'),
fetchOrder('o-404'),
]);
for (const r of settled) {
if (r.status === 'fulfilled') {
console.log('OK', r.value.id); // r.value: Order
} else {
console.log('FALHA', r.reason); // r.reason: any
}
}
OK o-1
FALHA Erro: Pedido o-404 não encontrado
r.reason tem tipo any, não unknown. Este é um pequeno erro na biblioteca padrão, então você ainda deve verificar instanceof Error antes de usar.
6.3. Promise.race e helper genérico timeout
Promise.race retorna o tipo união das Promises. Combinado com Promise<never>, temos um helper timeout que preserva o tipo:
export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timeout após ${ms}ms`)), ms),
);
return Promise.race([promise, timeout]);
}
const order = await withTimeout(fetchOrder('o-1'), 1000); // order: Order
Como timeout tem tipo Promise<never>, o resultado de race é Promise<T | never>, que é Promise<T>. never desaparece da tipo união. Este é justamente o significado: “este ramo nunca retorna um valor”.
Nota: O exemplo acima não cancela
setTimeoutquandopromisetermina primeiro. Em código production, você deve guardar o id do timer e chamarclearTimeoutemfinally. Outra opção é usarAbortSignal.timeout(ms)se a API subjacente suportarsignal.
7. Tipos para callbacks async
7.1. A armadilha () => void
TypeScript permite atribuir uma função que retorna Promise<void> a um tipo () => void. Isto parece inofensivo mas pode engolir erros:
function onOrderPaid(handler: (order: Order) => void) {
const order: Order = { id: 'o-1', customerId: 'c-1', total: 250_000, status: 'PAID' };
handler(order); // Promise retornada é ignorada
console.log('Handler chamado');
}
onOrderPaid(async (order) => {
await delay(10);
throw new Error('Falha ao enviar email'); // unhandled rejection!
});
Handler chamado
Erro: Falha ao enviar email (unhandled rejection, pode fazer crash do process Node)
onOrderPaid não sabe que o handler é async. Ele não faz await, então o erro não é capturado por ninguém. A forma de corrigir é declarar explicitamente que o handler pode ser async, depois fazer await nele:
type OrderHandler = (order: Order) => void | Promise<void>;
async function onOrderPaidSafe(handler: OrderHandler): Promise<void> {
const order: Order = { id: 'o-1', customerId: 'c-1', total: 250_000, status: 'PAID' };
await handler(order); // await num valor não-Promise ainda é válido
}
O tipo void | Promise<void> aceita handlers síncronos e assíncronos. await num valor síncrono simplesmente retorna o próprio valor.
7.2. async dentro de forEach
Este é o erro mais comum. forEach espera (item) => void, então ignora Promise:
async function payAll(list: Order[]): Promise<void> {
list.forEach(async (o) => {
await delay(20);
o.status = 'PAID';
});
console.log(list.map((o) => o.status)); // ainda ['PENDING', ...]
}
Há duas formas de corrigir, dependendo da necessidade:
// Sequencial: um pedido por vez
for (const o of list) {
await delay(20);
o.status = 'PAID';
}
// Paralelo: tipo de retorno é Promise<void[]>
await Promise.all(
list.map(async (o) => {
await delay(20);
o.status = 'PAID';
}),
);
Para capturar estes erros de tipo automaticamente, ative as rules @typescript-eslint/no-floating-promises e @typescript-eslint/no-misused-promises no typescript-eslint. O compilador TypeScript sozinho não reporta erros nestes casos.
8. Testando comportamento e tipos com Vitest
Vitest permite testar tanto valores em runtime quanto tipos em compile time:
// order-service.test.ts
import { describe, it, expect, expectTypeOf } from 'vitest';
import { fetchOrder, fetchCustomer } from './order-service';
import { withTimeout } from './with-timeout';
import { delay, type Order } from './models';
describe('order-service', () => {
it('retorna pedido quando existe', async () => {
await expect(fetchOrder('o-1')).resolves.toMatchObject({
id: 'o-1',
status: 'PENDING',
});
});
it('rejeita quando pedido não é encontrado', async () => {
await expect(fetchOrder('o-404')).rejects.toThrow('Pedido o-404 não encontrado');
});
it('withTimeout rejeita quando promise demora muito', async () => {
const slow = delay(200).then(() => 'concluído');
await expect(withTimeout(slow, 50)).rejects.toThrow('Timeout após 50ms');
});
it('preserva tipos corretos para Promise.all e withTimeout', () => {
expectTypeOf(fetchOrder).returns.resolves.toEqualTypeOf<Order>();
expectTypeOf(withTimeout(fetchOrder('o-1'), 100)).resolves.toEqualTypeOf<Order>();
expectTypeOf(
Promise.all([fetchOrder('o-1'), fetchCustomer('c-1')]),
).resolves.toMatchTypeOf<[Order, { name: string }]>();
});
});
Executar testes:
npx vitest run --typecheck
✓ order-service.test.ts (4 tests)
Test Files 1 passed (1)
Tests 4 passed (4)
Há três pontos importantes:
- Sempre
awaitantes deexpect(...).resolves/.rejects. Se esquecer, o teste termina antes de a Promise ser resolvida e sempre passa. .rejects.toThrow(...)verifica a mensagem do erro rejeitado.expectTypeOfnão executa lógica nenhuma em runtime. A flag--typecheckpede ao Vitest para executartsce confirmar os tipos. Se alguém mudarwithTimeoutpara retornarPromise<unknown>, este teste falha.
9. Erros comuns
| Erro | Consequência | Correção |
|---|---|---|
Esquecer await ao chamar função async |
Variável tem tipo Promise<Order> em vez de Order; erro é engolido |
Ativar no-floating-promises; ler o tipo atentamente ao passar o mouse |
if (fetchOrder(id)) |
Sempre true pois Promise é um objeto |
TypeScript 5.x reporta condição sempre verdadeira; faça await antes |
async dentro de forEach |
Não espera, erro não é capturado | for...of ou Promise.all(map) |
Acessar err.message em catch |
Erro de compilação quando strict ativado |
Estreitar tipo com instanceof Error |
Casting as Promise<Order> para dados de fetch |
Tipo “mente”, erro aparece em runtime | Validar dados com schema depois atribuir tipo |
O último erro merece mais explicação. await response.json() retorna Promise<any>. Escrever const order: Order = await res.json() compila, mas TypeScript não verifica nada. Dados vindos de fora do processo sempre precisam ser validados.
Nota: Nem toda função que retorna Promise precisa da palavra-chave
async. Se a função apenas passa uma Promise adiante, por exemploreturn repository.find(id), você pode omitirasync. Mantenhaasyncquando hátry/catchem volta deawaitna função. Se omitirasynceawaitneste caso, ocatchnão captará o erro de rejeição.
10. Conclusão
Funções async sempre retornam Promise<T>. Declare este tipo explicitamente para APIs públicas. Use Awaited<T> quando precisar do tipo “desembrulhado” de uma Promise. Promise.all preserva tipo de tupla se você passar um array literal ou usar as const. Combine Promise.race com Promise<never> para escrever helpers genéricos sem perder tipos.
Sobre erros, Promise não carrega tipo de erro. A variável em catch é unknown, e o padrão Result é a escolha quando você quer que erros de negócio apareçam explicitamente no tipo. Finalmente, tenha cuidado com callbacks de tipo () => void e forEach. Ative as rules ESLint sobre Promise para capturar automaticamente erros que o compilador deixa passar.