Meilleures pratiques en programmation TypeScript

Meilleures pratiques en programmation TypeScript

Cet article explique les meilleures pratiques en programmation TypeScript.

Ce guide expose des pratiques recommandées pour exploiter les types de TypeScript, réduire les bugs et écrire un code plus lisible.

YouTube Video

Meilleures pratiques en programmation TypeScript

Le plus grand avantage de TypeScript est 'prévenir les bugs grâce aux types et rendre l'intention du code explicite'.

Les meilleures pratiques ne sont pas seulement des règles, mais un ensemble de principes pour écrire un code sûr, lisible et maintenable. Ci-dessous, nous présentons des meilleures pratiques couramment utilisées en TypeScript avec des exemples concrets.

Évitez any et attribuez toujours des définitions de types explicites

Tout d'abord, examinons le point consistant à « éviter any et à attribuer des types significatifs ».

any désactive complètement la vérification de type, ce qui va à l'encontre de l'objectif de TypeScript. Au lieu d'utiliser any juste pour faire fonctionner votre code, il est important de fournir des types aussi descriptifs que possible.

1// Bad
2function parse(data: any) {
3  return data.value;
4}

Ce code peut accepter n'importe quelle valeur, alors les erreurs à l'exécution ne peuvent pas être évitées.

1// Good
2type ParsedData = {
3  value: string;
4};
5
6function parse(data: ParsedData): string {
7  return data.value;
8}

En définissant les types, vous clarifiez l'intention des entrées et sorties et améliorez la sécurité.

Définissez toujours explicitement les structures d'objet avec type ou interface.

Ensuite, examinons le point consistant à « toujours définir explicitement les structures d'objet en utilisant type ou interface ».

Si vous utilisez des objets de manière ad hoc, leur structure peut devenir ambiguë. Extrayez toujours les types pour la réutilisabilité et la maintenabilité.

1// Bad
2function createUser(user: { name: string; age: number }) {
3  console.log(user.name);
4}

Même dans de petits morceaux de code, il est important de prendre l'habitude de séparer les types.

1// Good
2type User = {
3  name: string;
4  age: number;
5};
6
7function createUser(user: User): void {
8  console.log(user.name);
9}

En nommant les types, la compréhension de l'ensemble du code devient beaucoup plus facile.

Utilisez les types unions pour représenter précisément tous les états possibles.

Si vous utilisez des types string ou number seuls pour les conditions, des valeurs inattendues peuvent passer. En utilisant les types union, vous pouvez représenter uniquement les états autorisés au niveau du type.

1// Bad
2function setStatus(status: string) {
3  console.log(status);
4}

Dans ce code, des chaînes dénuées de sens ou des valeurs incorrectes ne peuvent pas être détectées lors de la compilation.

1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4function setStatus(status: Status): void {
5  console.log(status);
6}

En utilisant les types union, vous pouvez éliminer de manière fiable les 'états impossibles' à la compilation. En conséquence, la sécurité des branches conditionnelles et la fiabilité du code s'amélioreront.

Gérez explicitement null et undefined.

Ensuite, examinons le point consistant à « gérer explicitement null et undefined ».

En TypeScript, il est important d'exprimer dans le type la possibilité d'absence d'une valeur. Si vous laissez des ambiguïtés, cela peut conduire à des erreurs à l'exécution.

1type User = {
2  name: string;
3  email?: string;
4};

L'attribut email peut ne pas exister, vous devez donc traiter ce cas.

1function printEmail(user: User): void {
2  if (user.email) {
3    console.log(user.email);
4  }
5}

Vérifiez toujours les valeurs optionnelles avant de les utiliser.

N'utilisez les assertions de type (as) qu'en dernier recours.

Ensuite, examinons le point consistant à « ne pas abuser des assertions de type ».

Les assertions de type contournent temporairement la vérification de type de TypeScript pour déclarer : 'Je sais que cette valeur est de ce type.'. En abuser réduit la sécurité liée aux types.

1// Bad
2const value = input as string;

Dans ce code, même si la valeur réelle n'est pas une chaîne de caractères, aucune erreur ne se produit, ce qui peut entraîner des erreurs à l'exécution. Comme illustré dans le code suivant, choisissez de vérifier en toute sécurité à l'aide des protections de type (type guards) en priorité.

1// Good
2function isString(value: unknown): value is string {
3  return typeof value === "string";
4}
5
6if (isString(input)) {
7  console.log(input.toUpperCase());
8}

Les protections de type (type guards) sont un mécanisme permettant de déterminer un type en toute sécurité lors de la vérification de la valeur réelle. En donnant la priorité aux protections de type plutôt qu'aux assertions de type, vous pouvez plus facilement éviter les erreurs à l'exécution.

Ne comptez pas trop sur l'inférence de type pour les types de retour.

Ensuite, examinons le point consistant à « ne pas trop s’appuyer sur l’inférence pour les types de retour ».

L'inférence de type de TypeScript est puissante, mais il est plus sûr d'annoter explicitement les types de retour des fonctions publiques. Cela minimise l'impact potentiel des modifications futures.

1// Bad
2function sum(a: number, b: number) {
3  return a + b;
4}

Indiquez clairement votre intention même dans les petites fonctions.

1// Good
2function sum(a: number, b: number): number {
3  return a + b;
4}

Préciser les types de retour augmente la stabilité de votre API.

Gérez les entrées de manière sûre avec unknown.

Ensuite, examinons le point consistant à « accepter les entrées externes de manière sûre en utilisant unknown ».

Pour les entrées externes telles que les API, le JSON ou les saisies utilisateur, utilisez unknown au lieu de any. Ce faisant, vous vous assurez que toutes les valeurs sont validées, garantissant ainsi la sécurité des types.

1// Bad
2function handleResponse(data: any) {
3  console.log(data.id);
4}

Voici comment valider les types avec unknown.

 1// Good
 2function handleResponse(data: unknown): void {
 3  if (
 4    typeof data === "object" &&
 5    data !== null &&
 6    "id" in data
 7  ) {
 8    console.log((data as { id: number }).id);
 9  }
10}

unknown ne peut pas être utilisé tel quel ; c'est un type qui nécessite une validation. C'est particulièrement efficace lors du traitement des entrées externes.

Rendez vos types plus expressifs en combinant de petits types.

Ensuite, examinons le point consistant à « augmenter l'expressivité en combinant de petits types ».

Définir de grands types d'un seul bloc réduit la lisibilité et la maintenabilité. Divisez vos types en unités significatives et combinez-les selon les besoins.

 1type Id = number;
 2
 3type UserProfile = {
 4  id: Id;
 5  name: string;
 6};
 7
 8type UserWithStatus = UserProfile & {
 9  status: "active" | "inactive";
10};

Considérez les types comme des composants pour organiser votre conception.

type et interface

Avantages de interface

type et interface servent tous deux à définir des types, mais leurs usages et caractéristiques diffèrent. En les utilisant à bon escient, l'intention derrière vos définitions de type devient plus claire.

 1// Bad
 2type User = {
 3  id: number;
 4  name: string;
 5};
 6
 7type AdminUser = {
 8  id: number;
 9  name: string;
10  role: "admin";
11};

Si vous dupliquez ainsi des parties communes, votre code devient fragile face aux modifications.

1// Good
2interface User {
3  id: number;
4  name: string;
5}
6
7interface AdminUser extends User {
8  role: "admin";
9}

interface est idéal pour les conceptions impliquant l'extension (extends) et convient pour exprimer la 'forme' des objets.

Avantages de type

En revanche, type est plus expressif et adapté à la gestion des unions et intersections.

1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4type ApiResponse<T> =
5  | { status: "success"; data: T }
6  | { status: "error"; message: string };

type convient pour exprimer les états, les options ou les combinaisons.

Conseils pour choisir entre type et interface

En règle générale, utilisez interface pour les structures d'objet et les contrats, et type quand vous avez besoin de l'expressivité des unions, intersections ou opérations sur les types.

Les deux fonctionnent de manière similaire, mais il est important de choisir selon ce qui exprime au mieux la raison d'être du type.

Traitez vos types comme de la documentation.

Enfin, examinons le point consistant à « écrire les types comme de la documentation ».

De bonnes définitions de type transmettent plus d'informations que n'importe quel commentaire. Il est important de viser un état où « la spécification peut être comprise simplement en regardant les types ».

1type ApiError = {
2  code: number;
3  message: string;
4  retryable: boolean;
5};

De cette façon, l’un des principaux atouts de TypeScript est que les définitions de types peuvent servir de document de spécification.

Résumé

Les bonnes pratiques TypeScript ne consistent pas à être excessivement strict. L'essence est de clarifier l'intention via les types et d'écrire un code robuste face au changement.

En accumulant de petites règles dans le développement quotidien, vous obtenez sur le long terme des bénéfices comme 'des revues facilitées', 'moins de bugs' et 'une meilleure compréhension pour vous-même et les autres à l'avenir'.

D'abord, en réfléchissant 'comment puis-je exprimer cela avec des types ?', vous écrirez du code de haute qualité 'à la TypeScript'.

Vous pouvez suivre l'article ci-dessus avec Visual Studio Code sur notre chaîne YouTube. Veuillez également consulter la chaîne YouTube.

YouTube Video