
Atualmente, o desenvolvimento de qualquer aplicativo front-end moderno é mais complicado do que o hello world
level, no qual a equipe está trabalhando (cuja composição é alterada periodicamente), exige muito da qualidade da base de código. Para manter o nível de qualidade do código no nível adequado, nós, na equipe front - end do #gostgroup , mantemos a atualização e não temos medo de usar tecnologias modernas que mostram seus benefícios práticos em projetos de empresas de diversas escalas .
A digitação estática e seus benefícios no exemplo do TypeScript foram muito discutidos em vários artigos e, portanto, hoje vamos nos concentrar em tarefas mais aplicadas que os desenvolvedores de front-end enfrentam como exemplo da pilha favorita da nossa equipe (React + Redux).
"Eu não entendo como você geralmente vive sem uma digitação estrita. O que você está fazendo. Débito por dias a fio?" - o autor não é conhecido por mim.
"não, escrevemos tipos o dia todo" - meu colega.
Ao escrever código no TypeScript (a seguir, no texto, a pilha de assuntos será implícita), muitos reclamam que precisam gastar muito tempo escrevendo tipos manualmente. Um bom exemplo de ilustração do problema é a função do conector de connect
da react-redux
. Vamos dar uma olhada no código abaixo:
type Props = { a: number, b: string; action1: (a: number) => void; action2: (b: string) => void; } class Component extends React.PureComponent<Props> { } connect( (state: RootStore) => ({ a: state.a, b: state.b, }), { action1, action2, }, )(Component);
Qual é o problema aqui? O problema é que, para cada nova propriedade injetada através do conector, devemos descrever o tipo dessa propriedade no tipo geral de propriedades do componente (React). Não é uma ocupação muito interessante, você diz que ainda deseja coletar o tipo de propriedades do conector em um tipo, que é "conectado" uma vez ao tipo geral de propriedades do componente. Eu tenho boas notícias para você. O TypeScript permite fazer isso hoje! Você está pronta? Vamos lá!
O poder do TypeScript
O TypeScript não fica parado e está em constante evolução (pelo que eu amo). A partir da versão 2.8, apareceu uma função muito interessante (tipos condicionais), que permite o mapeamento de tipos com base em expressões condicionais. Não vou entrar em detalhes aqui, mas apenas deixe um link para a documentação e insira um pedaço de código como ilustração:
type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object"; type T0 = TypeName<string>;
Como essa função ajuda no nosso caso. InferableComponentEnhancerWithProps
a descrição dos tipos de biblioteca InferableComponentEnhancerWithProps
react-redux
, é possível encontrar o tipo InferableComponentEnhancerWithProps
, responsável por garantir que os tipos de propriedades injetadas não caiam no tipo externo de propriedades do componente que devemos definir explicitamente ao instanciar o componente. O tipo InferableComponentEnhancerWithProps
possui dois parâmetros gerais: TInjectedProps
e TNeedsProps
. Estamos interessados no primeiro. Vamos tentar "puxar" esse tipo desse conector!
type TypeOfConnect<T> = T extends InferableComponentEnhancerWithProps<infer Props, infer _> ? Props : never ;
E puxando diretamente o tipo em um exemplo real do repositório (que você pode clonar e executar um programa de teste lá):
import React from 'react'; import { connect } from 'react-redux'; import { RootStore, init, TypeOfConnect, thunkAction, unboxThunk } from 'src/redux'; const storeEnhancer = connect( (state: RootStore) => ({ ...state, }), { init, thunkAction: unboxThunk(thunkAction), } ); type AppProps = {} & TypeOfConnect<typeof storeEnhancer> ; class App extends React.PureComponent<AppProps> { componentDidMount() { this.props.init(); this.props.thunkAction(3000); } render() { return ( <> <div>{this.props.a}</div> <div>{this.props.b}</div> <div>{String(this.props.c)}</div> </> ); } } export default storeEnhancer(App);
No exemplo acima, dividimos a conexão com o repositório (Redux) em dois estágios. No primeiro estágio, obtemos um componente de ordem superior storeEnhancer
(também conhecido como InferableComponentEnhancerWithProps
) para extrair dele tipos de propriedades InferableComponentEnhancerWithProps
usando nosso TypeOfConnect
auxiliar TypeOfConnect
e combinando (através da interseção de &
types) os tipos de propriedades obtidos com os tipos de propriedades nativas do componente. No segundo estágio, simplesmente decoramos nosso componente original. Agora, o que você adicionar ao conector, ele cairá automaticamente nos tipos de propriedade do componente. Ótimo, o que queríamos alcançar!
Um leitor atento percebeu que os geradores de ação (para simplificar, simplificamos o termo ação) com efeitos colaterais (criadores de ações thunk) passam por processamento adicional usando a função unboxThunk
. O que causou uma medida adicional? Vamos acertar. Primeiro, vejamos a assinatura de uma ação desse tipo usando o exemplo de um programa do repositório:
const thunkAction = (delay: number): ThunkAction<void, RootStore, void, AnyAction> => (dispatch) => { console.log('waiting for', delay); setTimeout(() => { console.log('reset'); dispatch(reset()); }, delay); };
Como pode ser visto na assinatura, nossa ação não retorna imediatamente a função de destino, mas primeiro uma intermediária, que o redux-middleware
atende para permitir efeitos colaterais em nossa função principal. Porém, ao usar esta função no formulário conectado nas propriedades do componente, a assinatura dessa função é reduzida, excluindo a função intermediária. Como descrevê-lo em tipos? Precisa de uma função especial de conversão. Novamente, o TypeScript mostra seu poder. Primeiro, descrevemos o tipo que remove a função intermediária da assinatura:
CutMiddleFunction<T> = T extends (...arg: infer Args) => (...args: any[]) => infer R ? (...arg: Args) => R : never ;
Aqui, além dos tipos condicionais, usamos uma inovação muito recente do TypeScript 3.0, que nos permite deduzir o tipo de um número arbitrário (parâmetros de resto) de argumentos de função. Veja a documentação para detalhes. Agora resta cortar a parte excedente de nossa ação de uma maneira bastante rígida:
const unboxThunk = <Args extends any[], R, S, E, A extends Action<any>>( thunkFn: (...args: Args) => ThunkAction<R, S, E, A>, ) => ( thunkFn as any as CutMiddleFunction<typeof thunkFn> );
Passando a ação por esse conversor, obtemos a assinatura necessária na saída. Agora a ação está pronta para uso no conector.
Assim, através de manipulações simples, reduzimos nosso trabalho manual ao escrever código digitado em nossa pilha. Se formos um pouco mais longe, também podemos simplificar a digitação de ação e redutores, como fizemos no redux-modus .
PS Ao usar a ligação dinâmica de ações no conector por meio da função e redux.bindActionCreators
precisaremos cuidar da digitação mais adequada desse utilitário (possivelmente escrevendo nosso próprio wrapper).
Atualização 0
Se alguém achou que essa solução era conveniente, você pode gostar dela, para que o utilitário de tipo seja adicionado ao @types/react-redux
.
Atualização 1
Mais alguns tipos com os quais você não precisa especificar explicitamente o tipo de adereços injetados do hock. Apenas pegue o hoki e retire os tipos deles:
export type BasicHoc<T> = (Component: React.ComponentType<T>) => React.ComponentType<any>; export type ConfiguredHoc<T> = (...args: any[]) => (Component: React.ComponentType<T>) => React.ComponentType<any>; export type BasicHocProps<T> = T extends BasicHoc<infer Props> ? Props : never; export type ConfiguredHocProps<T> = T extends ConfiguredHoc<infer Props> ? Props : never; export type HocProps<T> = T extends BasicHoc<any> ? BasicHocProps<T> : T extends ConfiguredHoc<any> ? ConfiguredHocProps<T> : never ; const basicHoc = (Component: React.ComponentType<{a: number}>) => class extends React.Component {}; const configuredHoc = (opts: any) => (Component: React.ComponentType<{a: number}>) => class extends React.Component {}; type props1 = HocProps<typeof basicHoc>;
Update2
O tipo do assunto agora está em @types/react-redux
( ConnectedProps ).