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.
TypeScript fournit une gamme d'outils supplémentaires pour améliorer l'expérience et la productivité des développeurs :
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 :
parse
. Lorsque vous tapez data.
dans votre éditeur, vous ne recevrez pas de suggestions correctes sur les méthodes disponibles pour data
.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.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.
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 :
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.
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
.
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.
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.
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.
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);
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
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 :
authenticate
peut être dangereux, car nous pouvons oublier de transmettre des arguments importants à la fonction.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); }
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)); }
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.
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!