Retorno dinâmico em funções usando React e TypeScript
Olá, me chamo Leona Souza, trabalho na Gamers Club há pouco mais de dois anos como desenvolvedora front-end e decidi comentar sobre uma situação que aconteceu comigo durante o trabalho. Os nomes dos arquivos e variáveis utilizados no artigo são fictícios, mas a aplicação dos conceitos apresentados é 100% utilizável.
Há muito tempo escrevi uma função que faz uma consulta ao banco de dados via endpoint e retorna um tipo de registro. Recentemente surgiu a necessidade de alterar esse endpoint para que tenha mais de um possível retorno dependendo de uma variável na url. Ao invés de escrever uma função diferente para consumir o endpoint com variável (visto que os retornos são diferentes), pensei se seria possível reaproveitar a função tornando dinâmico o tipo do retorno.
No front-end com TypeScript usar uma função com mais de um possível retorno pode ser um pouco problemático, pois o TS interpreta o valor da função como “Tipo1 | Tipo2” e isso pode gerar alguns conflitos de tipo nos estados do React. Vamos analisar dois componentes fictícios chamados Listagem.tsx e services.ts.
services.ts
export interface Usuario {
id: number;
nome: string;
}
export interface Paginacao<T> {
resultados: T[];
pagina: number;
total: number;
}
export const PAGINACAO_INICIAL = {
resultados: [],
pagina: 1,
total: 10
};
interface Props {
devePaginar?: boolean;
}
export const getUsuarios = async (
props: Props
): Promise<Usuario[] | Paginacao<Usuario>> => {
const { devePaginar } = props || false
// Há outras formas de converter para string, mas vamos simplificar
const params = devePaginar ? "?paginado=true" : "";
const retorno = await fetch(`url.com/usuarios${params}`);
return retorno.json();
};
Esse código funciona perfeitamente, porém na hora de passar o resultado para algum estado do React teremos que especificar “na mão” qual é o tipo esperado (type assertion), pois erros podem acontecer.
Listagem.tsx:
const [usuarios, setUsuarios] = useState<Usuario[]>([]);
const [paginados, setPaginados] = useState<Paginacao<Usuario>>(
PAGINACAO_INICIAL
);
const carregarDados = async () => {
// getUsuarios retorna Usuario[] | Paginacao<Usuario>
const listaNormal = await getUsuarios();
setUsuarios(listaNormal); // Erro. O estado espera apenas Usuario[]
const listaPaginada = await getUsuarios({ devePaginar: true });
setPaginados(listaPaginada); // Erro. O estado espera apenas Paginacao<Usuario>
};
useEffect(() => {
carregarDados();
}, []);
Uma forma simples de resolver esse problema:
setUsuarios(listaNormal as Usuario[]);
setPaginados(listaPaginada as Paginacao<Usuario>);
No entanto, pensando em escalabilidade, seria necessário fazer isso em todos os casos em que a função é chamada. Particularmente acredito que é melhor fazer a conferência apenas uma vez, direto na função. E, já que quem “reclama” é o TS, vamos ensiná-lo que o retorno pode ser dinâmico.
services.ts:
interface Props {
devePaginar?: boolean;
}
export const getUsuarios = async <
T extends Props, // *1
P = T["devePaginar"] extends true ? Paginacao<Usuario> : Usuario[] // *2
>(
props?: T
): Promise<P> => { // *3
const { devePaginar } = props || {};
const params = devePaginar ? "?paginado=true" : "";
const retorno = await fetch(`url.com/usuarios${params}`);
return retorno.json();
};
// *Explicações à seguir
Essa função deixou de ter Promise<Usuario[] | Paginacao<Usuario>> como retorno fixo e agora tem um retorno dinâmico dependendo da configuração que passamos na hora de consumir o endpoint. Se utilizarmos a variável devePaginar como true a função retorna apenas Promise<Paginacao<Usuario>>. Se não utilizarmos uma configuração, ou devePaginar seja false, a função retorna apenas Promise<Usuario[]>. Para isso estamos dizendo ao TS que vamos trabalhar com dois tipos chamados T e P, que são genéricos.
- 1) T é um objeto com a chave opcional devePaginar do tipo boolean.
- 2) Depois definimos o valor de P, que é dinâmico, utilizando um operador ternário. Dizemos que, se existir a propriedade devePaginar com valor true no nosso tipo T, significa que P terá o valor Paginacao<Usuario>. Em qualquer outro caso o valor de P será Usuario[].
- 3) Agora basta dizer que o retorno da função é Promise<P> e o TS fará a conferência em todos os casos que formos utilizar a função.
const [usuarios, setUsuarios] = useState<Usuario[]>([]);
const [paginados, setPaginados] = useState<Paginacao<Usuario>>(
PAGINACAO_INICIAL
);
const carregarDados = async () => {
const listaNormal = await getUsuarios();
setUsuarios(listaNormal); // Sem erros
const listaPaginada = await getUsuarios({ devePaginar: true });
setPaginados(listaPaginada); // Sem erros
};
Caso queira ver o código completo, ou até mesmo fazer modificações para testes, visite: https://codesandbox.io/s/quirky-thunder-hzfqs4
Referência e leitura adicional
https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html