Salve Salve! 🖖
Vou compartilhar com vocês o pouquinho que eu sei sobre tipagens condicionais e espero conseguir fazer você entender mais sobre isso.
Nota
Este post não serve de documentação e não tem como objetivo ser o mais completo possível. Ele representa apenas a minha visão sobre a utilização de conditional types que obtive ao longo do tempo.
Introdução
Não é de hoje que o Typescript vem se tornando cada vez mais utilizado no cenário do frontend, muitos devs já começam seus projetos já com o Typescript habilitado (eu sou assim).
Dando uma breve contextualizada. Typescript é um linguagem de programação fortemente tipada construída encima do Javascript. Tipar variáveis, objetos, funções, etc facilita demais no descobrimento de propriedades durante o desenvolvimento.
Tipagens condicionais não são comumente utilizadas porém há casos de uso que elas se encaixam muito bem. Como o proprio nome já diz tudo, se trata de uma tipagem baseada em uma condição. Vou te mostrar um caso de uso que eu acho muito legal em um contexto react, é um pouquinho mais avançado mas espero conseguir explicar direitinho 😀
Show me the code!
Sem mais enrolação vamos desenvolver durante este post a tipagem da propriedade as
do componente Button
em react. Você já deve ter se deparado com algum componente de uma biblioteca de componentes como ChakraUI e MaterialUI a utilização dessa propriedade.
Não pretendo implementar exatamente igual a essas bibliotecas mas vou exemplificar aqui como fazer uma tipagem condicional básica para satisfazer o componente. Basicamente o componente Button
poderá receber o nome de um elemento HTML que será o utilizado como wrapper dentro do componente.
Abaixo temos o código do componente sem tipagens:
const Button = (({ as, children, ...restProps }) => {
const Element = as || "button";
return (
<Element {...restProps}>{children}</Element>
);
})
export const App = () => {
return (
<Button type="button">Abrir</Button>
);
}
A partir desse código criaremos nossas tipagens. Vamos começar criando a tipagem do componente Button
como queremos utiliza-lo.
interface IButton {
as: ??;
icon?: React.Element;
}
Acima temos a interface básica do nosso componente, a gente espera receber a propriedade as
com valor do elemento HTML que envolverá nosso botão. Além disso adicionei uma outra propriedade que será opcional mas sem mistérios nela, é só pra exemplificar com outras propriedades.
Desafio
Aqui nos deparamos com nosso desafio. Queremos que quando o código abaixo for chamado:
<Button as="button">Enviar</Button>
No autocomplete das propriedades que este componente pode receber, deverá aparecer as propriedades do elemento HTML que passamos na propriedade as
, ou seja, no exemplo, button. Além das outras problemas que colocamos na interface IButton
.
Typescript Generics
Precisamos entender mais algumas coisas a mais sobre Typescript. Generics é uma forma de enviar tipos dinâmicos para dentro de alguma interface. Você já deve ter utilizado ele se você já trabalhou com axios. Olha o código abaixo:
await axios.get<IResponseData>('https://github.com/guiselair')
Neste exemplo, foi passado um generic informando qual será a tipagem do retorno da requisição get. Utilizaremos desse principio para construir nossas tipagens condicionais.
Além disso é possível fazer com generics condições, mais ou menos assim, “Se for tal tipo, pega tal tipagem, senao, pega outra” ou até “Se o tipo foi este, pega este tipo”. Não se preocupe que abaixo vou explicar melhor.
Tipando propriedade as
Vamos atualizar a interface do IButton
:
interface IButton<E extends React.ElementType = React.ElementType> {
as: E;
icon?: React.ElementType;
}
Com o tipo acima, agora garantimos que o que for escrito dentro da propriedade as
será um elemento HTML válido, caso não for um erro semelhante a este será exibido:
🔥 Type '"div1"' is not assignable to type 'ElementType | undefined’
A primeira parte do nosso desafio esta pronta, tipar a propriedade as
. Agora temos que definir as tipagens das propriedade deste componente levando em consideração o valor recebido pelo as
.
Tipando props
do componente Button
Para pegar todas as propriedades do elemento HTML recebido precisamos utilizar um código meio complicadinho de entender mas vamos com calma. O código abaixo, faz uma condicional em formato de generics e caso seja uma das opções (keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>
) retorna uma tipagem do tipo JSX.LibraryManagedAttributes
.
Segunda a documentação da tipagem, este tipo JSX.LibraryManagedAttributes
define uma transformação nas props
do componente. Então basicamente, este tipo vai unir outros tipos e retornar uma união entre eles.
type PropsOf<
E extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>
> = JSX.LibraryManagedAttributes<E, React.ComponentPropsWithRef<E>>;
Obs: Estou simplificando aqui a explicação por dois motivos, este código você não precisara decorar e este é o conhecimento que eu tenho até então sobre isso 😄
Agora que já conseguimos pegar todos os tipos do elemento HTML precisamos mesclar o tipo IButton
com as as propriedades do elemento. Para isso vamos criar um novo type:
export type IButtonProps<E extends React.ElementType> = IButton<E> &
Omit<PropsOf<E>, keyof IButton>;
O código acima extende a interface IButton
com o resultado da expressão (Omit<PropsOf<E>, keyof IButton>
). O auxiliar Omit
remove da tipagem PropsOf<E>
as chaves do tipo IButton
para não haver conflitos de nomes de propriedades, isso poderia geral erros no Typescript.
Com este trecho pronto agora basta a gente adicionar este novo tipo como retorno do nosso componente. Isso é feito no trecho de código abaixo:
export const Button = <E extends React.ElementType = "button">({
as,
...restProps
}: IButtonProps<E>) => {
const Element = as || "button";
return <Element {...restProps}>{children}</Element>
};
E pronto!! Agora nosso componente já deve aceitar elemento HTML recebidos pela propriedade as
e adicionar todas as propriedades deste elemento no nosso componente Button
além das nossas propriedades personalizadas.
Abaixo esta o código completo do componente:
import React from "react";
type PropsOf<
E extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>
> = JSX.LibraryManagedAttributes<E, React.ComponentPropsWithRef<E>>;
export interface IButton<E extends React.ElementType = React.ElementType> {
as?: E;
icon?: React.Element;
}
export type IButtonProps<E extends React.ElementType> = IButton<E> &
Omit<PropsOf<E>, keyof IButton>;
export const Button = <E extends React.ElementType = "button">({
as,
...restProps
}: IButtonProps<E>) => {
const Element = as || "button";
return <Element {...restProps} />;
};
Exemplo em execução
Top demais!! 😻
Chegamos ao fim de mais um post e então, gostou?
Espero que eu tenho conseguido ajudar um pouquinho você a compreender melhor o Typescript e tirar sua curiosidade de como pode ser feito a propriedade as
que aparece em algumas bibliotecas. Achou que eu falei algo errado? Ficou com dúvidas? Vamos discutir melhor sobre nos comentários.
See you soon!