Desafio TypeScript

Resultado de operação assíncrona com carregamento

Enunciado

Modele o resultado de uma operação assíncrona que pode estar carregando, ter sucesso (com dados) ou falhar (com erro). Crie uma união discriminada Resultado<T> com os estados 'carregando', 'sucesso' e 'erro'. Implemente uma função mensagem(resultado: Resultado<string>): string que retorna uma mensagem apropriada. Além disso, crie uma função map que transforma o valor de sucesso, mantendo os outros estados inalterados. Use switch exaustivo com never em ambas as funções.

Requisitos

  • A união Resultado<T> deve ter três variantes: { status: 'carregando' }, { status: 'sucesso'; dados: T } e { status: 'erro'; erro: string }.
  • A função mensagem deve retornar: 'Carregando...' para carregando, Sucesso: ${dados} para sucesso, e Erro: ${erro} para erro.
  • A função map deve receber um Resultado<T> e uma função (dados: T) => U, retornando Resultado<U>. Se for sucesso, aplica a função; caso contrário, retorna o mesmo estado (com tipo ajustado).
  • Ambas as funções devem usar switch exaustivo com never no default.

Código inicial

type Resultado<T> =
  | { status: 'carregando' }
  | { status: 'sucesso'; dados: T }
  | { status: 'erro'; erro: string };

function mensagem(resultado: Resultado<string>): string {
  // implemente
}

function map<T, U>(resultado: Resultado<T>, fn: (dados: T) => U): Resultado<U> {
  // implemente
}

Saída esperada

Mensagem: Carregando...
Mensagem: Sucesso: Olá
Mensagem: Erro: Falha
Após map: Sucesso: OLÁ
Após map (carregando): Carregando...
Após map (erro): Erro: Falha
Ver dica

No map, para os casos 'carregando' e 'erro', retorne o mesmo objeto, mas o TypeScript pode precisar de uma asserção de tipo para ajustar o genérico. Use as Resultado<U> ou construa um novo objeto.

Mostrar solução
type Resultado<T> =
  | { status: 'carregando' }
  | { status: 'sucesso'; dados: T }
  | { status: 'erro'; erro: string };

function mensagem(resultado: Resultado<string>): string {
  switch (resultado.status) {
    case 'carregando':
      return 'Carregando...';
    case 'sucesso':
      return `Sucesso: ${resultado.dados}`;
    case 'erro':
      return `Erro: ${resultado.erro}`;
    default:
      const exaustivo: never = resultado;
      return exaustivo;
  }
}

function map<T, U>(resultado: Resultado<T>, fn: (dados: T) => U): Resultado<U> {
  switch (resultado.status) {
    case 'carregando':
      return { status: 'carregando' };
    case 'sucesso':
      return { status: 'sucesso', dados: fn(resultado.dados) };
    case 'erro':
      return { status: 'erro', erro: resultado.erro };
    default:
      const exaustivo: never = resultado;
      return exaustivo;
  }
}

// Testando
const carregando: Resultado<string> = { status: 'carregando' };
const sucesso: Resultado<string> = { status: 'sucesso', dados: 'Olá' };
const erro: Resultado<string> = { status: 'erro', erro: 'Falha' };

console.log('Mensagem:', mensagem(carregando));
console.log('Mensagem:', mensagem(sucesso));
console.log('Mensagem:', mensagem(erro));

const sucessoMaiusculo = map(sucesso, (s) => s.toUpperCase());
console.log('Após map:', mensagem(sucessoMaiusculo));

const carregandoMap = map(carregando, (s) => s.toUpperCase());
console.log('Após map (carregando):', mensagem(carregandoMap));

const erroMap = map(erro, (s) => s.toUpperCase());
console.log('Após map (erro):', mensagem(erroMap));

Passo a passo

  1. Definimos Resultado<T> como uma união discriminada genérica com três variantes, usando status como discriminante.
  2. A função mensagem recebe Resultado<string> e usa switch para retornar a mensagem adequada para cada status.
  3. No case 'sucesso', acessamos resultado.dados com segurança, pois o tipo foi refinado.
  4. O default com never garante exaustividade.
  5. A função map é genérica em T e U. Ela recebe um Resultado<T> e uma função de transformação.
  6. Dentro do switch, para 'carregando', retornamos um novo objeto { status: 'carregando' } (o tipo é compatível com Resultado<U>).
  7. Para 'sucesso', aplicamos fn aos dados e retornamos um novo objeto de sucesso com o resultado.
  8. Para 'erro', retornamos um novo objeto de erro com a mesma mensagem.
  9. Testamos com diferentes estados e imprimimos as mensagens antes e depois do map.

Por que funciona

A união discriminada permite que o TypeScript refine o tipo com base no campo status. Isso possibilita acessar dados apenas no caso de sucesso e erro apenas no caso de erro, sem erros de compilação. O uso de never no default assegura que todos os casos foram tratados, o que é crucial para manter a robustez ao adicionar novos estados. A função map demonstra como transformar o valor de sucesso mantendo a estrutura da união, um padrão comum em programação funcional.

Erros comuns

  • Esquecer de retornar um novo objeto nos casos 'carregando' e 'erro': se retornar o mesmo objeto, o TypeScript pode reclamar de incompatibilidade de tipo genérico. Exemplo: return resultado; em 'carregando' causa erro porque Resultado<T> não é atribuível a Resultado<U>.
  • Não usar never no default: perde a verificação de exaustividade.
  • Acessar dados fora do case 'sucesso': erro de compilação, pois dados não existe nas outras variantes.
  • Tipar o discriminante como string: o refinamento não funciona; use literais.

Outra forma de resolver

Poderia usar if/else aninhados, mas o switch é mais limpo. Para o map, uma alternativa é usar as Resultado<U> nos casos não sucesso, mas isso é menos seguro. Prefira construir novos objetos.

Saída esperada

Mensagem: Carregando...
Mensagem: Sucesso: Olá
Mensagem: Erro: Falha
Após map: Sucesso: OLÁ
Após map (carregando): Carregando...
Após map (erro): Erro: Falha