Como usar Type Predicate do TypeScript no React?
Vamos imaginar um cenário em que você tem um componente chamado User
que é responsável por renderizar um usuário.
Esse componente recebe um objeto chamado user que contém as informações do usuário, sendo que estas informações seguem esse tipo:
type User = {
name: string;
age: number;
email?: string;
}
Com base na interface, a gente estabelece que o usuário tem nome, idade e email, mas o email não é obrigatório.
Agora falando da implementação do componente User
, além de exibir as informações do usuário, ele também deve truncar o email caso este seja maior que 20 caracteres seguidos de três pontos (...), caso contrário, deve exibir o email completo. A gente pode fazer isso da seguinte forma:
type UserProps = {
user: User;
};
function User({ user }: UserProps) {
return (
<div>
<h1>{user.name}</h1>
<h2>{user.age}</h2>
{user.email && <h3>{user.email.length > 20 ? `${user.email.substring(0, 20)}...` : user.email}</h3>}
</div>
);
}
Legal, esta implementação funciona perfeitamente e atende o que é preciso. Mas e se o email passa a ter um novo requisito e agora deva ser exibido somente se possuir o domínio @gmail.com, teríamos que alterar a implementação para:
function User({ user }: UserProps) {
return (
<div>
<h1>{user.name}</h1>
<h2>{user.age}</h2>
{user.email && user.email.includes('@gmail.com') && <h3>{user.email.length > 20 ? `${user.email.substring(0, 20)}...` : user.email}</h3>}
</div>
);
}
Como podemos ver, o componente está ficando cada vez mais complexo e menos legível com o tempo. A gente pode resolver isso utilizando de funções auxiliares, como por exemplo:
function User({ user }: UserProps) {
function shouldShowEmail(email?: string): boolean {
if (!email) return false;
return email.includes('@gmail.com');
}
function truncateEmail(email: string): string {
return email.length > 20 ? `${email.substring(0, 20)}...` : email;
}
return (
<div>
<h1>{user.name}</h1>
<h2>{user.age}</h2>
{shouldShowEmail(user.email) && <h3>{truncateEmail(user.email)}</h3>}
</div>
);
}
E aí que entra nosso problema, a partir desta refatoração, o typescript passa a reclamar que a função truncateEmail
espera receber um email que seja string
porém estamos informando um valor que é string
ou undefined
. Mas espera, a gente não está validando
no trecho anterior se o email tem valor na função shouldShowEmail
? Como a gente pode falar pro TypeScript que é certeza que naquele momento email será uma string
? Podemos fazer isso:
function User({ user }: UserProps) {
return (
<div>
...
{shouldShowEmail(user.email) && <h3>{truncateEmail(user.email as string)}</h3>}
</div>
);
}
Se você pensou nessa solução, você está certo e não tem problema nenhum em usar a keyword as
para dizer imperativamente que o email é uma string. O desenvolvedor sabe mais do que o TypeScript e tudo bem você usar o as
quando possível.
Porém, e se o trecho do truncate passar a usar mais funções que dependam deste valor ou que seja necessário manipular o email, teremos que passar o as
todas as vezes? Sim! E como podemos resolver isso? Aí que aparece o Type Predicate e a keyword
is
, vamos dar uma olhada:
function User({ user }: UserProps) {
// Agora estamos falando para esta função que, se o valor retornar false
// significa que email não é uma string e se retornar true, email é uma string
function shouldShowEmail(email?: string): email is string {
if (!email) return false;
return email.includes('@gmail.com');
}
...
return (
<div>
...
{shouldShowEmail(user.email) && <h3>{truncateEmail(user.email)}</h3>}
</div>
);
}
Aha! E esta é a solução ideal, conseguimos falar para o typescript que a partir do momento que o shouldShowEmail
retornar true
todas as referências seguintes para o valor user.email serão string
sem a necessidade de usar as
todas as vezes.
Bem massa, né?
É isso, galera, este foi meu primeiro post, espero que tenha sido de ajuda pra vocês, fiquem a vontade para dar feedback ou complementarem com mais exemplos. Valeu!