Programming

Rendre TypeScript vraiment « fortement typé » | de Maksim Zemskov | septembre 2023

[ad_1]

Comment contrôler le type « n’importe quel » pour atteindre une sécurité de type ultime

Image générée à l’aide de Midjourney

TypeScript prétend être un langage de programmation fortement typé construit sur JavaScript, offrant de meilleurs outils à n’importe quelle échelle. Cependant, TypeScript inclut le 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 du any tapez dans des 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 :

  • 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 le any tapez votre base de code, vous perdez tous les avantages ci-dessus. Le any type est une faille dangereuse dans le système de types, et son utilisation désactive toutes les capacités de vérification de type et 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 à l’intérieur du parse fonction. Quand tu tapes data. dans votre éditeur, vous ne recevrez pas de suggestions correctes sur les méthodes disponibles pour data.
  • Dans le premier cas, il existe un TypeError: data.split is not a function erreur 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 deuxième cas, le res2 La variable a également le any taper. Cela signifie qu’une seule utilisation de any peut avoir un effet en cascade sur une grande partie d’une base de code.

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

Il est important de connaître les sources du any tapez une base de code car vous écrivez explicitement any n’est pas la seule option. Malgré tous nos efforts pour éviter d’utiliser le any tapez, il peut parfois se faufiler implicitement dans une base de code.

Il existe quatre sources principales de any tapez 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 Considérations clés dans tsconfig et Amélioration des types de bibliothèques standard pour les deux premiers points. Veuillez les consulter pour améliorer la sécurité des caractères dans vos projets.

Cette fois, nous nous concentrerons sur les outils automatiques de contrôle de l’apparence du any tapez une base de code.

ESLint est un outil d’analyse statique populaire que les développeurs Web utilisent pour garantir les meilleures pratiques et le formatage du code. Il peut appliquer des styles de codage et trouver du code qui ne respecte pas certaines directives.

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

La configuration la plus courante pour typescript-eslint est comme suit:

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

Cette configuration permet eslint pour 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 recommended Le préréglage contient un ensemble soigneusement sélectionné de règles ESLint pour améliorer l’exactitude du code. Bien qu’il soit recommandé d’utiliser l’intégralité du préréglage, pour cet article, nous nous concentrerons uniquement sur le no-explicit-any règle.

Le mode strict de TypeScript empêche l’utilisation de anymais ça n’empêche pas any d’être explicitement utilisé. Le no-explicit-any la règle permet d’interdire l’écriture manuelle any n’importe 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 {}

L’objectif principal de cette règle est d’empêcher l’utilisation de any dans toute 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éconseillé.

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 inférence de type. Cependant, cela est encore loin d’atteindre la sécurité de type ultime.

Bien que nous ayons traité explicitement des anyil y en a encore beaucoup d’implicites any 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

Les deux variables pokemons et settings ont été implicitement donnés any taper. Ni l’un ni l’autre no-explicit-any ni le mode strict de TypeScript ne nous avertiront dans ce cas. Pas encore.

Cela se produit parce que les types pour response.json() et JSON.parse() proviennent de la bibliothèque standard de TypeScript, où ces méthodes ont un any annotation. 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 problèmes implicites. any dans une base de code.

D’une manière générale, il existe encore de nombreuses façons de any pour se faufiler dans notre code.

Idéalement, nous aimerions un paramètre dans TypeScript qui oblige le compilateur à se plaindre de toute variable ayant reçu le any tapez pour quelque raison que ce soit. 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 typescript-eslint brancher. Ce mode fonctionne 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 les capacités de vérification de type de TypeScript. Par exemple, une règle peut rechercher toutes les variables portant le any tapez, quelle que soit la façon dont any a été obtenu.

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

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-eslintajouter parserOptions à la configuration ESLint. Ensuite, remplacez le recommended préréglé avec recommended-type-checked. Ce dernier préréglage ajoute environ 17 nouvelles règles puissantes. Pour cet article, nous nous concentrerons sur cinq d’entre eux seulement.

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

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

// ❌ 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`.

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

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 TypeScript type de rétrécissement ou une bibliothèque de validation telle que Zod ou Superstructure. Par exemple, écrivons le parseFormValues fonction 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 permis de passer le any tapez comme argument à une fonction qui accepte unknowncar cela ne pose aucun problème de sécurité.

L’écriture de fonctions de validation de données peut s’avérer 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);

Le no-unsafe-assignment La règle recherche les affectations de variables dans lesquelles une valeur a le any taper. 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 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

Grace à no-unsafe-assignment règle, nous pouvons attraper le any tapez avant même de passer formValues autre part. 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.

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

Le no-unsafe-call la règle nous empêche d’appeler une variable avec le any tapez 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 le authenticate la fonction peut être dangereuse, car nous pouvons oublier de lui transmettre des arguments importants.
  • Lire le name propriété de la userInfo l’objet est dangereux, comme il le 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 pouvez augmenter les types de bibliothèques toi-même. 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);
}

Le no-unsafe-return la règle aide à ne pas retourner accidentellement le any tapez à 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

Le parseForm La fonction peut entraîner des erreurs d’exécution dans n’importe quelle partie du programme où elle est utilisée puisque la valeur analysée n’est pas vérifiée. Le no-unsafe-return La règle empêche 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 crée 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 tsc compilateur. Par exemple, sur notre projet le plus récent avec environ 1,5 million de lignes de code TypeScript, tapez la vérification via tsc prend environ 11 minutes, tandis que le temps supplémentaire requis pour le démarrage des règles sensibles au type d’ESLint n’est que d’environ deux minutes.

La sécurité supplémentaire fournie par l’utilisation de règles d’analyse statique sensibles au type vaut le compromis pour notre équipe. Sur les petits projets, cette décision est encore plus facile à prendre.

Contrôler l’utilisation de any dans les projets TypeScript est crucial pour obtenir une sécurité de type et une qualité de code optimales. En utilisant le typescript-eslint plugin, les développeurs peuvent identifier et éliminer toute occurrence du any tapez leur base de code, ce qui donne 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’utiliser any 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!

[ad_2]

Source link

Related Articles

Back to top button