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
mensagemdeve retornar: 'Carregando...' para carregando,Sucesso: ${dados}para sucesso, eErro: ${erro}para erro. - A função
mapdeve receber umResultado<T>e uma função(dados: T) => U, retornandoResultado<U>. Se for sucesso, aplica a função; caso contrário, retorna o mesmo estado (com tipo ajustado). - Ambas as funções devem usar
switchexaustivo comnevernodefault.
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
- Definimos
Resultado<T>como uma união discriminada genérica com três variantes, usandostatuscomo discriminante. - A função
mensagemrecebeResultado<string>e usaswitchpara retornar a mensagem adequada para cada status. - No
case 'sucesso', acessamosresultado.dadoscom segurança, pois o tipo foi refinado. - O
defaultcomnevergarante exaustividade. - A função
mapé genérica emTeU. Ela recebe umResultado<T>e uma função de transformação. - Dentro do
switch, para'carregando', retornamos um novo objeto{ status: 'carregando' }(o tipo é compatível comResultado<U>). - Para
'sucesso', aplicamosfnaos dados e retornamos um novo objeto de sucesso com o resultado. - Para
'erro', retornamos um novo objeto de erro com a mesma mensagem. - 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 porqueResultado<T>não é atribuível aResultado<U>. - Não usar
nevernodefault: perde a verificação de exaustividade. - Acessar
dadosfora docase 'sucesso': erro de compilação, poisdadosnã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