DEV Community

Cover image for Propriedade as em componentes React com Typescript
Guilherme Selair
Guilherme Selair

Posted on • Edited on

Propriedade as em componentes React com Typescript

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>>;
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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>
};
Enter fullscreen mode Exit fullscreen mode

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} />;
};
Enter fullscreen mode Exit fullscreen mode

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!

Referências

  1. React polymorphic components with TypeScript | by Iskander Samatov | ITNEXT
  2. Polymorphic as prop for React components with TypeScript (github.com)

Top comments (0)