TypeScript pratique. React + Redux

Je ne comprends pas comment vous vivez généralement sans taper strictement. Qu'est-ce que tu fais. Débit toute la journée?

Actuellement, le développement de toute application frontale moderne est plus compliqué que le niveau hello world , sur lequel l'équipe travaille (dont la composition change périodiquement), exige beaucoup de la qualité de la base de code. Pour maintenir le niveau de qualité du code au bon niveau, nous, l' équipe front - end de #gostgroup , nous tenons à jour et n'avons pas peur d'appliquer des technologies modernes qui montrent leur avantage pratique dans des projets d' entreprises de différentes échelles .


Le typage statique et ses avantages sur l'exemple de TypeScript ont été beaucoup discutés dans divers articles, et donc aujourd'hui nous nous concentrerons sur des tâches plus appliquées auxquelles les développeurs frontaux sont confrontés comme un exemple de la pile préférée de notre équipe (React + Redux).


"Je ne comprends pas comment vous vivez généralement sans taper strictement. Que faites-vous? Débitez pendant des jours?" - l'auteur ne m'est pas connu.


"non, nous écrivons des types toute la journée" - mon collègue.


Lors de l'écriture de code en TypeScript (ci-après dans le texte, la pile de sujets sera implicite), beaucoup se plaignent de devoir passer beaucoup de temps à écrire des types manuellement. Un bon exemple illustrant le problème est la fonction de connect connexion de la react-redux . Jetons un œil au code ci-dessous:


 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); 

Quel est le problème ici? Le problème est que pour chaque nouvelle propriété injectée via le connecteur, nous devons décrire le type de cette propriété dans le type général des propriétés du composant (React). Pas une profession très intéressante, dites-vous, vous voulez toujours pouvoir collecter le type de propriétés du connecteur en un seul type, qui se "connecte" une fois au type général de propriétés du composant. J'ai de bonnes nouvelles pour toi. TypeScript vous permet de le faire dès aujourd'hui! Êtes-vous prêt? C'est parti!


La puissance de TypeScript


TypeScript ne reste pas immobile et évolue constamment (ce que j'adore). À partir de la version 2.8, une fonction très intéressante (types conditionnels) est apparue, qui permet de mapper des types basés sur des expressions conditionnelles. Je n'entrerai pas dans les détails ici, mais je laisse juste un lien vers la documentation et j'insère un morceau de code comme illustration:


 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>; // "string" type T1 = TypeName<"a">; // "string" type T2 = TypeName<true>; // "boolean" type T3 = TypeName<() => void>; // "function" type T4 = TypeName<string[]>; // "object" 

Comment cette fonction aide dans notre cas. react-redux regardant la description des types de bibliothèques react-redux , vous pouvez trouver le type InferableComponentEnhancerWithProps , qui est chargé de s'assurer que les types de propriétés injectées ne tombent pas dans le type externe de propriétés de composant que nous devons définir explicitement lors de l'instanciation du composant. Le type InferableComponentEnhancerWithProps a deux paramètres généraux: TInjectedProps et TNeedsProps . Nous nous intéressons au premier. Essayons de "tirer" ce type de ce connecteur!


 type TypeOfConnect<T> = T extends InferableComponentEnhancerWithProps<infer Props, infer _> ? Props : never ; 

Et en tirant directement le type sur un exemple réel du référentiel (que vous pouvez cloner et y exécuter un programme de test):


 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); 

Dans l'exemple ci-dessus, nous divisons la connexion au référentiel (Redux) en deux étapes. À la première étape, nous obtenons un composant de storeEnhancer ordre storeEnhancer (alias InferableComponentEnhancerWithProps ) pour en extraire les types de propriétés InferableComponentEnhancerWithProps à l'aide de notre TypeOfConnect assistance TypeOfConnect et en combinant davantage (via l'intersection de & types) les types de propriété obtenus avec les types de propriété natifs du composant. Dans la deuxième étape, nous décorons simplement notre composant d'origine. Maintenant, quoi que vous ajoutiez au connecteur, il tombera automatiquement dans les types de propriétés de composant. Génial, ce que nous voulions réaliser!


Un lecteur attentif a remarqué que les générateurs d'action (pour simplifier, nous simplifions le terme d'action) avec des effets secondaires (créateurs d'action unboxThunk ) subissent un traitement supplémentaire en utilisant la fonction unboxThunk . Qu'est-ce qui a causé une telle mesure supplémentaire? Faisons les choses correctement. Tout d'abord, regardons la signature d'une telle action en utilisant l'exemple d'un programme du référentiel:


 const thunkAction = (delay: number): ThunkAction<void, RootStore, void, AnyAction> => (dispatch) => { console.log('waiting for', delay); setTimeout(() => { console.log('reset'); dispatch(reset()); }, delay); }; 

Comme le montre la signature, notre action ne renvoie pas immédiatement la fonction cible, mais d'abord une fonction intermédiaire, que le redux-middleware ramasse pour activer les effets secondaires dans notre fonction principale. Mais lors de l'utilisation de cette fonction sous forme connectée dans les propriétés du composant, la signature de cette fonction est réduite, à l'exclusion de la fonction intermédiaire. Comment le décrire en types? Besoin d'une fonction de convertisseur spéciale. Encore une fois, TypeScript montre sa puissance. Tout d'abord, nous décrivons le type qui supprime la fonction intermédiaire de la signature:


 CutMiddleFunction<T> = T extends (...arg: infer Args) => (...args: any[]) => infer R ? (...arg: Args) => R : never ; 

Ici, en plus des types conditionnels, nous utilisons une innovation très récente de TypeScript 3.0, qui nous permet de déduire le type d'un nombre arbitraire (paramètres de repos) d'arguments de fonction. Consultez la documentation pour plus de détails. Reste maintenant à couper l'excédent de notre action de manière assez rigide:


 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> ); 

En passant l'action à travers un tel convertisseur, nous obtenons la signature dont nous avons besoin à la sortie. L'action est maintenant prête à être utilisée dans le connecteur.


Ainsi, grâce à de simples manipulations, nous réduisons notre travail manuel lors de l'écriture de code tapé sur notre pile. Si nous allons un peu plus loin, nous pouvons également simplifier le typage des actions et des réducteurs, comme nous l'avons fait dans redux-modus .


PS Lorsque vous utilisez la liaison dynamique d'actions dans le connecteur via la fonction et redux.bindActionCreators nous devrons nous occuper d'un typage plus approprié de cet utilitaire (éventuellement en écrivant notre propre wrapper).


Mise à jour 0
Si quelqu'un pensait que cette solution était pratique, alors vous pouvez l' aimer pour que l'utilitaire type soit ajouté au @types/react-redux .


Mise à jour 1
Quelques autres types avec lesquels vous n'avez pas besoin de spécifier explicitement le type d'accessoires injectés du jarret. Prenez simplement le hoki et retirez-en les types:


 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>; // {a: number} type props2 = HocProps<typeof configuredHoc>; // {a: number} 

Update2
Le type du sujet est maintenant dans @types/react-redux ( ConnectedProps ).

Source: https://habr.com/ru/post/fr431452/


All Articles