React and Typescript logos

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

--

--