React e TypeScript: O dilema defaultProps

Em outras palavras: “O problema de um milhão de reais”

⭐️ Créditos

Vamos supor que você queira usar / esteja usando React e com isso, tenha tomado a decisão de usar JavaScript tipado… e escolha o TypeScript para esse trabalho. Em primeira mão, parabéns por esta decisão em primeiro lugar, pois a sua vida como dev irá melhorar muito a partir de agora, principalmente graças a tipagem estática e DX de primeira qualidade! De qualquer forma, você começará a desenvolver suas primeiras partes do seu aplicativo React com o TypeScript. Tudo vai ficar impecável, até chegar a este primeiro grande problema. Lidando com defaultProps em componentes.

Esse artigo é baseado no TypeScript 2.9 e usa o modo strict. Se você não usa o modo strict, ligue-o o quanto antes, porque não usar o modo strict é como trair sua namorada e você não quer fazer isso, certo? (Se você estiver na fase de migração gradual de JS para TS, o nonStrict é OK!)

Vou demonstrar aqui o problema e como resolvê-lo através de componentes class. Vamos definir um componente Button, com a seguinte API:

  • onClick, o manipulador de cliques
  • color, a cor que será usada
  • type, o tipo do botão button ou submit

Vamos anotar color e type como opcionais, porque eles serão definidos via defaultProps, então o consumidor do nosso componente não precisa fornecê-los.

Implementação do componente Button

Agora, ao usarmos nosso componente, nós teremos sugestões para as propriedades obrigatórias e opcionais em tempo real/intellisense:

Essa DX / Intellisense no JSX / templates — cortesia do TypeScript 👌❤️

Tudo funciona e é estaticamente tipado. Beleza! Nós podemos ir para casa agora. Bem, não tão rápido, meus amigos!

Nosso defaultProps em Button, não está tipado, pois a análise estática não pode inferir os tipos das definições genéricas de extensões de classes de suas propriedades estáticas. 🤓

Mas 🤔… afinal, o que isso quer dizer? 😤

Quer dizer:

  • você pode definir qualquer coisa para o seu static defaultProps
  • você está definindo as mesmas coisas duas vezes (tipos e implementação)

Nenhuma verificação de tipos para defaultProps:

Nenhuma verificação de tipo para defaultProps

Podemos corrigir isso extraindo as propriedades color e type, para separar a tipagem e, em seguida, usar a interseção de tipos para mapear nossas opções padrões para serem opcionais por meio da função auxiliar Partial, da biblioteca padrão do TS.

Então precisamos anotar explicitamente o nosso static defaultProps: DefaultProps, que nos levará a segurança de tipo adequada / DX dentro da nossa implementação defaultProps!

Verificação de tipo adequada para defaultProps na implementação do componente

A última coisa que eu costumo fazer é extrair defaultProps e initialState (se state for usado) em constantes separadas, o que também nos dará outro benefício → obter a definição de tipo a partir da implementação, que introduz menos clichê na sua base de código e apenas uma fonte de verdade → a implementação.

defaultProps — definindo como fonte a implementação para o sistema de tipos

Por hora, tudo beleza.

E se introduzirmos alguma lógica ao nosso componente?

Digamos que não queremos usar estilos inline, e com base na prop color, queremos gerar uma classe css apropriada com alguma definição de estilo pré-definida.

Vamos definir uma função resolveColorTheme, que aceitará nossa prop e como resultado, obteremos css className.

Componente Button com lógica para resolver className

Com isso, obteremos um erro de compilação! Ah não! pânico! 🔥😱

TS Error:
Type 'undefined' is not assignable to type '"blue" | "green" | "red"'
Erro de compilação introduzido por adições opcionais

Por que recebemos um erro agora? Bem, color é opcional, e estamos no modo strict, o que significa que a união de tipo é estendida por um tipo undefined/void, mas nossa função não aceita undefined. Este é um ótimo exemplo do compilador fazendo o seu melhor, que é tentar nos proteger e corrigir a execução adequada do programa (se lembra dos tempos undefined is not a function?).

Como resolver esse problema? O problema de um milhão de reais!

A partir de hoje, Junho de 2018/TypeScript 2.9, há 4 opções para corrigir isso:

  • Operador de declarações não-nulo
  • Chamada de tipo no componente
  • High Order Functions para definir defaultProps
  • Utilizar uma função para definir props

Vamos analisar cada opção abaixo, preparado? 💪

1. Operador de declarações não-nulo

Essa solução nem precisa de muito esforço mental, tudo o que você precisa fazer é dizer ao compilador: “Ei cara, isso não será null ou undefined, confie em mim que eu sou um humano… ehm 🤖…”. Você consegue resolver isso com o operador não-nulo !.

utilizando o operador não-nulo no método render

Isso pode ser ok para casos de uso simples (como API com poucas props, acessando props específicas somente no método de renderização), mas uma vez que seu componente começa a crescer, ele pode ficar bagunçado e confuso rapidamente. Além disso, você precisa checar o tempo todo que a prop é definida como defaultProps -> mais sobrecarga cognitiva para o desenvolvedor === bad DX / propenso a erros.

2. Chamada de tipo no componente

Então, como resolver nosso problema com a mitigação de todos os pittfals mencionados na primeira solução?

Podemos criar nosso componente por meio de uma classe anônima e atribuí-lo à uma constante, a qual iremos converter para o componente de resultado final com os tipos de props apropriados, mantendo todos os defaultProps conforme definido em nossa implementação.

Componentes via definição indireta

Isso resolve o nosso problema anterior, mas de alguma forma parece um hack para mim.

Podemos melhorar isso de alguma forma?

O TypeScript 2.8 introduziu um recurso muito poderoso chamado de tipos condicionais.

Vamos usar o recurso novo com uma abordagem mais funcional, bora?

3. High Order Functions para definir defaultProps

Podemos definir uma função-fábrica/função-de-alta-ordem (factory function/high order function), para declarar defaultProps e passar tipos condicionais para resolver corretamente nossa API das props.

A função-de-alta-ordem withDefaultProps é usada para implementar o componente com fluxo de tipos correto para defaultProps

Agora está falando minha língua! Nós não usamos a API do React para definir defaultProps explicitamente, isso é tratado pela nossa função withDefaultProps.

Também podemos omitir type DefaultProps se quisermos, e colocar os tipos inline dentro do nosso type Props. Todo o resto (inferência de tipos) é tratado pelo TypeScript.

Isso é incrível! 😎

Então nós terminamos por aqui?

Claro..hmm..peraí…e quanto aos Componentes Genéricos???

Ah não, esquecemos completamente desse caso de uso! 😅

E você pergunta:

Mas, com licença… O que é um componente genérico? 👇👀

Componentes Genéricos e seu uso. Cortesia do TypeScript 2.9 <Button />

Se quisermos usar esse padrão (com a função withDefaultProps) com props genéricos, nosso tipo genérico seria perdido, então se você quer definir um componente genérico, esta solução não é viável 😥.

Fora isso, tá tudo certo!

Mas, então, há algum padrão final? 👀😱⁉️

Bem, eu não sei se é definitivo, mas funciona e abrange todos os problemas mencionados anteriormente…

4. Utilizar uma função para definir props

Vamos contemplar os humildes padrões função-fábrica/função-de-identidade-de-escopo (factory function/closure identity function) com mapeamento de tipos condicionais (conditional types mapping).

Observe que estamos aproveitando construções de mapeamento de tipos, semelhante ao como fizemos para a função withDefaultProps, exceto que não mapeamos defaultProps para serem opcionais, pois eles não são opcionais na implementação de nosso componente.
função-fábrica createPropsGetter

Nossa função cria uma closure e com isso armazena/infere informações do tipo defaultProps via parâmetro genérico. Em seguida, a função retorna a mescla de props com defaultProps e na perspectiva de tempo de execução desse código, a mesma propriedade que nós passamos como argumento, é retornada, portanto, a API padrão do React é usada para aquisição/resolução de props em tempo de execução.

Observe também que estamos explicitamente configurando a prop children para nossa API pública, dizendo que nosso Button precisa ter um filho do tipo ReactNode. Se o consumidor do nosso componente não fornecer um ReactNode como filho, o compilador nos notificaria o erro em tempo de compilação. Gracias TypeScript!

Vamos usar isso na implementação do nosso componente!

Componente Button com defaultProps e props de implementação devidamente tipados

Mais explicações sobre o que está acontecendo:

createPropsGetter com o fluxo de tipos da implementação do nosso componente

Isso é muito irado, não acha? 😎

Essa solução final cobre todos os problemas anteriores:

  • não há necessidade de escapes usando o operador não-nulo (!)
  • não há necessidade de chamar o nosso componente com outros tipos, adicionando indireções (const Button adicional etc)
  • nós não temos que recriar a implementação do componente e, assim, perder todos os tipos no processo (função withDefaultProps)
  • funciona com componentes genéricos
  • fácil de raciocinar/seguir e preparado para o futuro (TypeScript 3.0)

TypeScript 3.0 🖖

Se você acha que a equipe do TypeScript não percebeu esse “problema de um milhão de reais”, você está completamente errado 😇.

Esses caras amam a comunidade e tentam nos fornecer o melhor verificador de tipos JS no planeta ❤️.

Então, sim, @drosenwasser criou uma issue recentemente no GitHub, sobre como melhorar o suporte para props padrões no JSX, que é direcionado para o TypeScript 3.0.

Finalizando

O TypeScript implementará uma maneira genérica (alimentada por tipos condicionais, sem strings mágicas ou acoplamento no compilador para tecnologia específica/React) de como obter props padrão e refletirá as mudanças no JSX, buscando as definições de funções-fábrica, que são responsáveis pela criação dos objetos do VirtualDom (para o React, createElement, para Preact, h, etc).

Essa também é uma das razões pelas quais estamos preservando a React API para definir defaultProps em nossa última solução.

Com isso dito, estamos no final da nossa jornada para resolver o problema de um milhão de reais!

É isso aí cupincha! Como sempre, se tiver alguma dúvida, pode me procurar no Twitter, @martin_hotell. Uma ótima tipagem estática para todos vocês e até a próxima! Felicidades! 🤙🏄‍🍻