Desafio TypeScript

Função genérica com Awaited e tratamento de erros tipados

Enunciado

Implemente uma função genérica async unwrap<T>(value: T | Promise<T>): Promise<Awaited<T>> que retorna o valor resolvido. Além disso, crie uma classe TimeoutError e uma função async withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> que rejeita com TimeoutError se a promise não resolver dentro de ms milissegundos. Teste ambos os casos e trate os erros adequadamente.

Requisitos

  • unwrap deve aceitar T | Promise<T> e retornar Promise<Awaited<T>>.
  • withTimeout deve lançar TimeoutError se o tempo exceder ms.
  • O catch deve diferenciar TimeoutError de outros erros.
  • O programa deve demonstrar um caso de sucesso e um caso de timeout.

Código inicial

class TimeoutError extends Error {
  // complete
}

async function unwrap<T>(value: T | Promise<T>): Promise<Awaited<T>> {
  // complete
}

async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  // complete
}

async function main() {
  // testes
}

main();

Saída esperada

Valor resolvido: 42
Timeout: operação excedeu 100ms
Valor resolvido: 10
Ver dica

Para withTimeout, use Promise.race entre a promise original e uma promise que rejeita após ms. Lembre-se de tipar o erro como unknown no catch.

Mostrar solução
class TimeoutError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "TimeoutError";
  }
}

async function unwrap<T>(value: T | Promise<T>): Promise<Awaited<T>> {
  return await value;
}

async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new TimeoutError(`operação excedeu ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

async function main(): Promise<void> {
  try {
    const resolved = await unwrap(Promise.resolve(42));
    console.log(`Valor resolvido: ${resolved}`);

    // Simula uma promise que demora 200ms
    const slow = new Promise<number>((resolve) => setTimeout(() => resolve(99), 200));
    await withTimeout(slow, 100); // deve lançar TimeoutError
  } catch (error: unknown) {
    if (error instanceof TimeoutError) {
      console.error(`Timeout: ${error.message}`);
    } else {
      console.error("Erro inesperado:", error);
    }
  }

  // Teste de sucesso com withTimeout
  try {
    const fast = new Promise<number>((resolve) => setTimeout(() => resolve(10), 50));
    const result = await withTimeout(fast, 100);
    console.log(`Valor resolvido: ${result}`);
  } catch (error: unknown) {
    if (error instanceof TimeoutError) {
      console.error(`Timeout: ${error.message}`);
    } else {
      console.error("Erro inesperado:", error);
    }
  }
}

main();

Passo a passo

  1. Definimos TimeoutError estendendo Error com name personalizado.
  2. unwrap usa await para resolver o valor, seja ele uma promise ou um valor direto; o tipo de retorno é Promise<Awaited<T>>.
  3. withTimeout cria uma promise que rejeita após ms com TimeoutError.
  4. Usamos Promise.race para correr a promise original e o timeout; a primeira que resolver ou rejeitar define o resultado.
  5. No main, testamos unwrap com uma promise que resolve 42.
  6. Testamos withTimeout com uma promise lenta (200ms) e timeout de 100ms — deve lançar TimeoutError.
  7. No catch, tratamos especificamente TimeoutError e exibimos a mensagem.
  8. Testamos withTimeout com uma promise rápida (50ms) e timeout de 100ms — deve resolver 10.

Por que funciona

Awaited<T> extrai o tipo resolvido de uma promise, permitindo que unwrap seja genérica e segura. Promise.race resolve ou rejeita com o primeiro resultado, então o timeout interrompe a espera. O tratamento de erros com instanceof garante que apenas TimeoutError seja capturado como timeout, mantendo outros erros visíveis.

Erros comuns

  • Não usar Promise.race corretamente: Promise.race([promise, timeout]) sem tipar o timeout como Promise<never> pode causar erro de tipo.
  • Esquecer de rejeitar a promise de timeout: setTimeout(() => resolve(), ms) faria o timeout resolver em vez de rejeitar.
  • No catch, assumir que error é Error e acessar .message diretamente — compilação falha porque error é unknown.
  • Não limpar o timeout após a promise original resolver: isso pode causar comportamento inesperado em programas longos, mas não afeta o resultado aqui.

Outra forma de resolver

Usar AbortController com fetch ou operações que suportam cancelamento:

const controller = new AbortController();
setTimeout(() => controller.abort(), ms);

Essa abordagem é preferível quando a operação subjacente pode ser cancelada de fato, evitando trabalho desnecessário.

Saída esperada

Valor resolvido: 42
Timeout: operação excedeu 100ms
Valor resolvido: 10