paint-brush
Rendre TypeScript vraiment "fortement typé"by@nodge
15,793
15,793

Rendre TypeScript vraiment "fortement typé"

Maksim Zemskov12m2023/09/10
Read on Terminal Reader

TypeScript fournit le type « Any » pour les situations où la forme des données n'est pas connue à l'avance. Cependant, une utilisation excessive de ce type peut entraîner des problèmes de sécurité des types, de qualité du code et d’expérience des développeurs. Cet article explore les risques associés au type « Any », identifie les sources potentielles de son inclusion dans une base de code et propose des stratégies pour contrôler son utilisation tout au long d'un projet.
featured image - Rendre TypeScript vraiment "fortement typé"
Maksim Zemskov HackerNoon profile picture
0-item
1-item

TypeScript prétend être un langage de programmation fortement typé construit sur JavaScript, offrant de meilleurs outils à n'importe quelle échelle. Cependant, TypeScript inclut any type, qui peut souvent se faufiler implicitement dans une base de code et entraîner la perte de nombreux avantages de TypeScript.


Cet article explore les moyens de prendre le contrôle de any type dans les projets TypeScript. Préparez-vous à libérer la puissance de TypeScript, pour atteindre une sécurité de type ultime et améliorer la qualité du code.

Inconvénients de l'utilisation de Any dans TypeScript

TypeScript fournit une gamme d'outils supplémentaires pour améliorer l'expérience et la productivité des développeurs :


  • Cela permet de détecter les erreurs dès le début de la phase de développement.
  • Il offre une excellente saisie semi-automatique pour les éditeurs de code et les IDE.
  • Il permet une refactorisation facile de grandes bases de code grâce à de fantastiques outils de navigation de code et à une refactorisation automatique.
  • Il simplifie la compréhension d'une base de code en fournissant une sémantique supplémentaire et des structures de données explicites via des types.


Cependant, dès que vous commencez à utiliser any type dans votre base de code, vous perdez tous les avantages énumérés ci-dessus. Le type any est une faille dangereuse dans le système de types, et son utilisation désactive toutes les capacités de vérification de type ainsi que tous les outils qui dépendent de la vérification de type. En conséquence, tous les avantages de TypeScript sont perdus : des bugs sont manqués, les éditeurs de code deviennent moins utiles, et bien plus encore.


Par exemple, considérons l'exemple suivant :


 function parse(data: any) { return data.split(''); } // Case 1 const res1 = parse(42); // ^ TypeError: data.split is not a function // Case 2 const res2 = parse('hello'); // ^ any


Dans le code ci-dessus :


  • Vous manquerez la saisie semi-automatique dans la fonction parse . Lorsque vous tapez data. dans votre éditeur, vous ne recevrez pas de suggestions correctes sur les méthodes disponibles pour data .
  • Dans le premier cas, il y a une TypeError: data.split is not a function car nous avons passé un nombre au lieu d'une chaîne. TypeScript n'est pas en mesure de mettre en évidence l'erreur car any désactive la vérification du type.
  • Dans le second cas, la variable res2 a également le type any . Cela signifie qu'une seule utilisation de any peut avoir un effet en cascade sur une grande partie d'une base de code.


Utiliser any n'est acceptable que dans des cas extrêmes ou pour des besoins de prototypage. En général, il est préférable d’éviter d’ any utiliser pour tirer le meilleur parti de TypeScript.

D’où vient n’importe quel type

Il est important d'être conscient des sources de type any dans une base de code, car l'écriture explicite any n'est pas la seule option. Malgré tous nos efforts pour éviter d'utiliser any type, il peut parfois se faufiler implicitement dans une base de code.


Il existe quatre sources principales de any type dans une base de code :

  1. Options du compilateur dans tsconfig.
  2. La bibliothèque standard de TypeScript.
  3. Dépendances du projet.
  4. Utilisation explicite de any dans une base de code.


J'ai déjà écrit des articles sur les considérations clés dans tsconfig et l'amélioration des types de bibliothèques standard pour les deux premiers points. Veuillez les consulter si vous souhaitez améliorer la sécurité des caractères dans vos projets.


Cette fois, nous nous concentrerons sur les outils automatiques permettant de contrôler l’apparence de any type dans une base de code.

Étape 1 : Utiliser ESLint

ESLint est un outil d'analyse statique populaire utilisé par les développeurs Web pour garantir les meilleures pratiques et le formatage du code. Il peut être utilisé pour appliquer des styles de codage et rechercher du code qui ne respecte pas certaines directives.


ESLint peut également être utilisé avec des projets TypeScript, grâce au plugin typesctipt-eslint . Très probablement, ce plugin a déjà été installé dans votre projet. Mais sinon, vous pouvez suivre le guide de démarrage officiel.


La configuration la plus courante pour typescript-eslint est la suivante :


 module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, };


Cette configuration permet à eslint de comprendre TypeScript au niveau de la syntaxe, vous permettant d'écrire des règles eslint simples qui s'appliquent aux types écrits manuellement dans un code. Par exemple, vous pouvez interdire l’utilisation explicite de any .


Le préréglage recommended contient un ensemble soigneusement sélectionné de règles ESLint visant à améliorer l'exactitude du code. Bien qu'il soit recommandé d'utiliser l'intégralité du préréglage, pour les besoins de cet article, nous nous concentrerons uniquement sur la règle no-explicit-any .

non-explicite-aucun

Le mode strict de TypeScript empêche l'utilisation de any implicite, mais il n'empêche any son utilisation explicite. La règle no-explicit-any permet d'interdire l'écriture manuelle any où dans une base de code.


 // ❌ Incorrect function loadPokemons(): any {} // ✅ Correct function loadPokemons(): unknown {} // ❌ Incorrect function parsePokemons(data: Response<any>): Array<Pokemon> {} // ✅ Correct function parsePokemons(data: Response<unknown>): Array<Pokemon> {} // ❌ Incorrect function reverse<T extends Array<any>>(array: T): T {} // ✅ Correct function reverse<T extends Array<unknown>>(array: T): T {}


Le but principal de cette règle est d’empêcher any utilisation au sein de l’équipe. C'est un moyen de renforcer l'accord de l'équipe sur le fait que l'utilisation de any dans le projet est découragée.


Il s'agit d'un objectif crucial car même une seule utilisation de any peut avoir un impact en cascade sur une partie importante de la base de code en raison de l'inférence de type . Cependant, cela est encore loin d’atteindre la sécurité de type ultime.

Pourquoi rien d'explicite ne suffit pas

Bien que nous ayons traité any explicitement utilisés, il existe encore de nombreux any implicites dans les dépendances d'un projet, y compris les packages npm et la bibliothèque standard de TypeScript.


Considérez le code suivant, que l'on retrouve probablement dans n'importe quel projet :


 const response = await fetch('https://pokeapi.co/api/v2/pokemon'); const pokemons = await response.json(); // ^? any const settings = JSON.parse(localStorage.getItem('user-settings')); // ^? any


pokemons et settings variables ont été implicitement attribués à any type. Ni no-explicit-any ni le mode strict de TypeScript ne nous avertiront dans ce cas. Pas encore.


Cela se produit parce que les types de response.json() et JSON.parse() proviennent de la bibliothèque standard de TypeScript, où ces méthodes ont une any explicite. Nous pouvons toujours spécifier manuellement un meilleur type pour nos variables, mais il existe près de 1 200 occurrences de any dans la bibliothèque standard. Il est presque impossible de se souvenir de tous les cas où any peut se faufiler dans notre base de code à partir de la bibliothèque standard.


Il en va de même pour les dépendances externes. Il existe de nombreuses bibliothèques mal typées dans npm, la plupart étant encore écrites en JavaScript. En conséquence, l’utilisation de telles bibliothèques peut facilement conduire à de nombreux any implicites dans une base de code.


En général, il existe encore de nombreuses façons de any faufiler dans notre code.

Étape 2 : amélioration des capacités de vérification de type

Idéalement, nous aimerions avoir un paramètre dans TypeScript qui oblige le compilateur à se plaindre de toute variable ayant reçu any type pour une raison quelconque. Malheureusement, un tel paramètre n’existe pas actuellement et ne devrait pas être ajouté.


Nous pouvons obtenir ce comportement en utilisant le mode de vérification de type du plugin typescript-eslint . Ce mode fonctionne conjointement avec TypeScript pour fournir des informations de type complètes du compilateur TypeScript aux règles ESLint. Avec ces informations, il est possible d'écrire des règles ESLint plus complexes qui étendent essentiellement les capacités de vérification de type de TypeScript. Par exemple, une règle peut rechercher toutes les variables de type any , quelle que soit la manière dont any a été obtenue.


Pour utiliser des règles sensibles au type, vous devez ajuster légèrement la configuration ESLint :


 module.exports = { extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, root: true, };


Pour activer l'inférence de type pour typescript-eslint , ajoutez parserOptions à la configuration ESLint. Ensuite, remplacez le préréglage recommended par recommended-type-checked . Ce dernier préréglage ajoute environ 17 nouvelles règles puissantes. Pour les besoins de cet article, nous nous concentrerons sur seulement 5 d’entre eux.

aucun argument dangereux

La règle no-unsafe-argument recherche les appels de fonction dans lesquels une variable de type any est passée en paramètre. Lorsque cela se produit, la vérification du type est perdue, ainsi que tous les avantages d’un typage fort.


Par exemple, considérons une fonction saveForm qui nécessite un objet comme paramètre. Supposons que nous recevions JSON, l'analysions et obtenions any type.


 // ❌ Incorrect function saveForm(values: FormValues) { console.log(values); } const formValues = JSON.parse(userInput); // ^? any saveForm(formValues); // ^ Unsafe argument of type `any` assigned // to a parameter of type `FormValues`.


Lorsque nous appelons la fonction saveForm avec ce paramètre, la règle no-unsafe-argument la signale comme dangereuse et nous oblige à spécifier le type approprié pour la variable value .


Cette règle est suffisamment puissante pour inspecter en profondeur les structures de données imbriquées dans les arguments de fonction. Par conséquent, vous pouvez être sûr que le passage d'objets en tant qu'arguments de fonction ne contiendra jamais de données non typées.


 // ❌ Incorrect saveForm({ name: 'John', address: JSON.parse(addressJson), // ^ Unsafe assignment of an `any` value. });


La meilleure façon de corriger l'erreur est d'utiliser le rétrécissement de type de TypeScript ou une bibliothèque de validation telle que Zod ou Superstruct . Par exemple, écrivons la fonction parseFormValues qui restreint le type précis de données analysées.


 // ✅ Correct function parseFormValues(data: unknown): FormValues { if ( typeof data === 'object' && data !== null && 'name' in data && typeof data['name'] === 'string' && 'address' in data && typeof data.address === 'string' ) { const { name, address } = data; return { name, address }; } throw new Error('Failed to parse form values'); } const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues saveForm(formValues);


Notez qu'il est autorisé de transmettre le type any comme argument à une fonction qui accepte unknown , car cela ne pose aucun problème de sécurité.


L'écriture de fonctions de validation de données peut être une tâche fastidieuse, en particulier lorsqu'il s'agit de grandes quantités de données. Par conséquent, il vaut la peine d’envisager l’utilisation d’une bibliothèque de validation de données. Par exemple, avec Zod, le code ressemblerait à ceci :


 // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); const formValues = schema.parse(JSON.parse(userInput)); // ^? { name: string, address: string } saveForm(formValues);


mission non dangereuse

La règle no-unsafe-assignment recherche les affectations de variables dans lesquelles une valeur est de type any . De telles affectations peuvent induire le compilateur en erreur en lui faisant croire qu'une variable a un certain type, alors que les données peuvent en réalité avoir un type différent.


Prenons l'exemple précédent d'analyse JSON :


 // ❌ Incorrect const formValues = JSON.parse(userInput); // ^ Unsafe assignment of an `any` value


Grâce à la règle no-unsafe-assignment , nous pouvons détecter any type avant même de transmettre formValues ailleurs. La stratégie de correction reste la même : nous pouvons utiliser le rétrécissement du type pour fournir un type spécifique à la valeur de la variable.


 // ✅ Correct const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues


accès membre sans danger et appel sans danger

Ces deux règles se déclenchent beaucoup moins fréquemment. Cependant, d'après mon expérience, ils sont très utiles lorsque vous essayez d'utiliser des dépendances tierces mal typées.


La règle no-unsafe-member-access nous empêche d'accéder aux propriétés d'un objet si une variable a le type any , car elle peut être null ou undefined .


La règle no-unsafe-call nous empêche d'appeler une variable de type any en tant que fonction, car ce n'est peut-être pas une fonction.


Imaginons que nous ayons une bibliothèque tierce mal typée appelée untyped-auth :


 // ❌ Incorrect import { authenticate } from 'untyped-auth'; // ^? any const userInfo = authenticate(); // ^? any ^ Unsafe call of an `any` typed value. console.log(userInfo.name); // ^ Unsafe member access .name on an `any` value.


Le linter met en évidence deux problèmes :

  • Appeler la fonction authenticate peut être dangereux, car nous pouvons oublier de transmettre des arguments importants à la fonction.
  • La lecture de la propriété name à partir de l'objet userInfo n'est pas sûre, car elle sera null si l'authentification échoue.


La meilleure façon de corriger ces erreurs est d’envisager d’utiliser une bibliothèque avec une API fortement typée. Mais si ce n’est pas une option, vous pouvezaugmenter vous-même les types de bibliothèques . Un exemple avec les types de bibliothèques fixes ressemblerait à ceci :


 // ✅ Correct import { authenticate } from 'untyped-auth'; // ^? (login: string, password: string) => Promise<UserInfo | null> const userInfo = await authenticate('test', 'pwd'); // ^? UserInfo | null if (userInfo) { console.log(userInfo.name); }


pas de retour dangereux

La règle no-unsafe-return permet de ne pas renvoyer accidentellement any type à partir d'une fonction qui devrait renvoyer quelque chose de plus spécifique. De tels cas peuvent induire le compilateur en erreur en lui faisant croire qu'une valeur renvoyée a un certain type, alors que les données peuvent en réalité avoir un type différent.


Par exemple, supposons que nous ayons une fonction qui analyse JSON et renvoie un objet avec deux propriétés.


 // ❌ Incorrect interface FormValues { name: string; address: string; } function parseForm(json: string): FormValues { return JSON.parse(json); // ^ Unsafe return of an `any` typed value. } const form = parseForm('null'); console.log(form.name); // ^ TypeError: Cannot read properties of null


La fonction parseForm peut entraîner des erreurs d'exécution dans n'importe quelle partie du programme où elle est utilisée, car la valeur analysée n'est pas vérifiée. La règle de no-unsafe-return évite de tels problèmes d’exécution.


Il est facile de résoudre ce problème en ajoutant une validation pour garantir que le JSON analysé correspond au type attendu. Utilisons la bibliothèque Zod cette fois :


 // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); function parseForm(json: string): FormValues { return schema.parse(JSON.parse(json)); }


Une note sur les performances

L'utilisation de règles de vérification de type entraîne une pénalité de performances pour ESLint, car il doit appeler le compilateur TypeScript pour déduire tous les types. Ce ralentissement est principalement perceptible lors de l'exécution du linter dans des hooks de pré-commit et dans CI, mais il n'est pas perceptible lorsque vous travaillez dans un IDE. La vérification du type est effectuée une fois au démarrage de l'IDE, puis met à jour les types à mesure que vous modifiez le code.


Il convient de noter que le simple fait de déduire les types fonctionne plus rapidement que l'invocation habituelle du compilateur tsc . Par exemple, sur notre projet le plus récent avec environ 1,5 million de lignes de code TypeScript, la vérification du type via tsc prend environ 11 minutes, tandis que le temps supplémentaire requis pour le démarrage des règles de type ESLint n'est que d'environ 2 minutes.


Pour notre équipe, la sécurité supplémentaire apportée par l’utilisation de règles d’analyse statique sensibles au type en vaut la peine. Sur les petits projets, cette décision est encore plus facile à prendre.

Conclusion

Contrôler l'utilisation de any les projets TypeScript est crucial pour obtenir une sécurité de type et une qualité de code optimales. En utilisant le plugin typescript-eslint , les développeurs peuvent identifier et éliminer toutes les occurrences de any type dans leur base de code, ce qui donne lieu à une base de code plus robuste et plus maintenable.


En utilisant des règles eslint sensibles au type, toute apparition du mot-clé any dans notre base de code sera une décision délibérée plutôt qu'une erreur ou un oubli. Cette approche nous évite d' any utiliser dans notre propre code, ainsi que dans la bibliothèque standard et les dépendances tierces.


Dans l'ensemble, un linter sensible au type nous permet d'atteindre un niveau de sécurité de type similaire à celui des langages de programmation typés statiquement tels que Java, Go, Rust et autres. Cela simplifie grandement le développement et la maintenance de grands projets.


J'espère que vous avez appris quelque chose de nouveau grâce à cet article. Merci pour la lecture!

Liens utiles