La nécessité de Typescript et ses limites
Publié le 8 juillet 2024
🇬🇧 Article available in englishTypescript c'est cool, mais c'est pas assez, et c'est chiant parfois.
- javascript
- typescript
- types
- type system
Certain(e)s sont déjà convaincu(e)s par Typescript donc, cet article ne va donc rien vous apprendre de nouveau. Mais, beaucoup ici sont aussi à la recherche de réponses. De réponses à la question:
Je suis dev web et je viens juste de m’en sortir avec Javascript. Pourquoi on me casse les pieds avec Typescript?
Je vais tenter, très humblement, d’apporter des éléments de réponse, basés sur de véritables situations. Je vais même aller un peu plus loin et parler des quelques moments où même Typescript ne pourra RIEN pour toi. C’est parti!
Posons un contexte
Développeur ou développeuse junior dans une ESN qui te paie 28 000 (vingt huit mille) euros brut dans le centre de Paris, tu tentes malgré tout de joindre les deux bouts. Tu travailles sur une application e-commerce avec beaucoup de code existant, code étant le fruit du travail de 59 développeurs ou développeuses qui ont travaillé sur ce projet avant toi (ton ESN a un turnover de fou, mais les managers ne comprennent pas pourquoi!).
![Meme qui indique: 'Si tu traitais tes employés correctement, peut-être que le turnover ne serait pas si élevé'](/_astro/turnover-meme.BVPWZ10i_bLjLD.webp)
Un nouveau sprint démarre (vous faîtes de “l’agile scrum”), et tu choisis de d’attaquer ce nouveau cycle de deux semaines en choisissant une tâche où tu dois restaurer le panier d’un utilisateur.
Tout se passe assez bien, puis à un moment, tu décides de coder une fonction qui permet d’afficher le total du rabais de tous les élements du panier qui a été sauvegardé, genre:
function getDiscountFromSavedCart(cart) {
const discountTotalValue = cart.cartItems.reduce((sum, cartItem) => {
return sum + cartItem.pricing.discount;
}, 0);
return discountTotalValue;
}
Tu t’apprêtes à écrire les tests unitaires et d’intégration pour ton code, mais ton PM (Product Manager) t’indique que tu dois envoyer ton code en production au plus vite! Donc, tu les négliges et hop, ça part!
Classique.
Le temps passe et un jour, ton équipe reçoit un rapport alarmant concernant une
alerte sur Datadog. Une erreur Javascript a été détectée plus de 15 000 fois sur
le site en production! Cette erreur est due au fait que le discount de certains
paniers affiche NaN
(Not A Number), ce qui est très frustrant pour les
clients.
Après investigation, tu te rends compte que certains éléments du catalogue
d’articles ont une valeur null
, au lieu de 0
, sur le discount. Comme tu es
un 10x engineer, tu envoies un fix super rapidement après la détection.
function getDiscountFromSavedCart(cart) {
const discountTotalValue = cart.cartItems.reduce((sum, cartItem) => {
if (cartItem.pricing.discount) {
return sum + cartItem.pricing.discount;
} else {
return sum;
}
}, 0);
return discountTotalValue;
}
Tout est carré. Le code part en prod. Sauf que maintenant, ton équipe reçoit un type différent d’alertes:
Uncaught TypeError: Cannot read properties of undefined (reading 'discount')
Pour une raison obscure de compatibilité avec un ancien système, certains
élements du catalogue n’ont pas d’objet pricing
(une information que personne
ne t’a communiqué, même le client avait oublié).
Tu envoies donc un deuxième patch:
function getDiscountFromSavedCart(cart) {
const discountTotalValue = cart.cartItems.reduce((sum, cartItem) => {
if (cartItem.pricing?.discount) {
return sum + cartItem.pricing.discount;
} else if (cartItem.pricing) {
return sum + cartItem.discount;
} else {
return sum;
}
}, 0);
return discountTotalValue;
}
Mais cette fois, tu fais deux nouveaux trucs:
- Tu écris des tests unitaires, qui vont vérifier que ta fonction a le
comportement attendu pour les possibles valeurs de l’objet
cart
. - Tu commentes ton code, pour expliquer que un
cartItem
contient ou non un objetpricing
avec ou non une valeurdiscount
.
Là, tu dois te dire qu’il y a un milliard de manières pour résoudre ce problème, manières qui ne passent pas forcément par du code. T’as 100% raison, mais prétendons que non. Code first.
Typescript va te permettre de vérifier le point 1 et le point 2 mentionnés plus haut en même temps.
Typescript ne remplace pas les tests unitaires, mais certains tests peuvent faire doublons avec un bon type system.
Comment Typescript peut t’aider?
Typescript c’est un language à 90% similaire à Javascript. On parle même de Typescript comme un superset de Javascript, parce que du code Javascript valide est du code Typescript valide. Les 10% de différences sont liés au fait que Typescript t’autorise à ajouter des annotations de types à ton code.
Les annotations de types, ce sont des informations qui vont indiquer le type de donnée (nombre, chaîne de caractères, etc…) attendu par une variable, un paramètres de fonction, où même, ce qu’une fonction retourne. Tu peux le voir comme un contrat que ton code doit respecter pour espérer fonctionner correctement.
// Le code ci dessous est valide en Javascript et Typescript
const user = { id: "10", name: "Christopher" };
// Le code ci dessous n'est valide que dans le cadre de Typescript
type User = { id: string; name: string };
const user: User = { id: "10", name: "Christopher" };
Ces annotations de type vont être utilisées par le compilateur Typescript (un
exécutable appelé tsc
), afin de vérifier si ton programme respecte bien les
contraintes que tu as établi dans le code. Si jamais quelque chose semble
incorrect, tsc
refuse de compiler ton programme et une erreur est retournée.
Le résultat de la compilation de tsc
, c’est juste ton code, sans les
annotations de type, et donc du Javascript valide.
Pour des raisons de simplicité, je vulgarise ici. Ce n’est pas exactement 100% ton code mais ca reste du Javascript.
Dans l’exemple evoqué en début d’article, si la variable cart
avait un type
associé, on aurait pu se rendre compte que la fonction manipule l’objet cart
d’une manière qui peut provoquer des erreurs en production. Cette information
serait connue depuis l’IDE car Typescript s’intègre très bien aux éditeurs de
code (via le
Language Server Protocol)
![Après avoir ajouté un type pour le paramètre `cart`, Typescript est capable de nous dire que la première version de notre fonction n'est pas valide comme elle oublie de considérer que l'objet `cart.princing` est peut-être absent.](/_astro/typescript-error.x9xySESl_Z154i4B.webp)
Sur l’image précédente, on a ajouté un type à la variable cart
. Quand on
applique ce type à la première version de la fonction
getDiscountFromSavedCart
, Typescript est capable de nous dire qu’on a surement
une erreur dans ce code.
C’est la puissance de ce que l’on appelle le compile-time type checking, le
fait qu’un outil comm tsc
va vérifier que les types et leurs usages dans le
code sont corrects AVANT que ton application tourne en production (ce qu’on
appelle le runtime
). Typescript fait un job assez décent sur ce point. Toute
une classe de bugs est minimisée grâce à cette pratique.
Aussi, le code est d’un coup plus clair à lire et à comprendre, malgré l’absence de documentation et de commentaires.
type CartItem = {
id: string;
discount: null | number;
pricing?: { discount: null | number };
};
type Cart = {
cartItems: CartItem[];
};
function getDiscountFromSavedCart(cart: Cart) {
const discountTotalValue = cart.cartItems.reduce((sum, cartItem) => {
if (cartItem.pricing?.discount) {
return sum + cartItem.pricing.discount;
} else if (cartItem.discount) {
return sum + cartItem.discount;
} else {
return sum;
}
}, 0);
return discountTotalValue;
}
Au premier regard, tu comprends ce que la variable cart
contient, et quelles
sont les contraintes appliqués à ce paramètre. Génial non?
Donc Typescript évite tous les bugs?
Non.
Très souvent, pour ne pas dire tout le temps, ton application va interagir avec
l’extérieur: des APIs, des cookies, le localStorage
, le sessionStorage
,
des entrées utilisateurs, etc…
![Ne jamais faire confiance aux input utilisateur](/_astro/user-input-meme.AkSVN5Ig_ZVzzHY.webp)
Dans ce cas, tu peux prévoir et avoir une idée de ce que ces systèmes vont envoyer à ton app, puis écrire des types Typescript afin que ton type system soit toujours cohérent. MAIS on est loin, très loin, du compile-time et donc Typescript ne peut RIEN pour t’aider à vérifier que tout est correct.
C’est une des grosses limites de Typescript, comparé à d’autres système de types, comme celui d’OCaml ou Rust. Une fois que l’application tourne, et fait appel à de la donnée externe (Input/Output), on est de nouveau exposé au genre d’erreurs que l’on voulait éviter de base. La solution ici, c’est d’implémenter une système de runtime type checking.
L’idée, c’est de créer une fonction, un validateur, qui va contrôler tout ce qui rentre dans ton application au runtime. Tu peux utiliser des trucs comme:
Certains utilitaires te permettent même de pouvoir générer un type Typescript à partir d’un validateur donné, histoire d’allier l’utile à l’agréable.
Tu veux un example?
import { z } from "zod";
const cartItemValidator = z.object({
id: z.string(),
discount: z.union([z.null(), z.number()]),
pricing: z
.object({
discount: z.union([z.null(), z.number()]),
})
.optional(),
});
type CartItem = z.infer<typeof cartItemValidator>;
const result = cartItemValidator.safeParse(foreignData);
if (result.success) {
const cartItem = result.data;
// Do something I guess?
} else {
// Show an error or something
}
Ici j’utilise Zod
. J’ai un validateur qui va vérifier au runtime qu’une donnée
inconnue correspond bien à ce que mon app considère comme étant un CartItem
.
Le validateur me permet aussi de génerer un type Typescript grâce aux
utilitaires que Zod
fournit. Pas mal non?
Tout n’est pas parfait
Maintenant que j’ai bien vendu le truc, je vais te donner quelques points sur pourquoi c’est parfois super chiant.
All or nothing
Le truc avec Typescript, c’est que pour garantir la safety d’un programme, TOUT doit être typé. Si tu laisses un seul point faible, tu diminues l’efficacité de Typescript.
Le type any
Typescript donne des outils pour te tirer dans le cul tout seul comme un grand.
any
en fait parti. Il s’agit d’un type qui accepte n’importe quelle donnée.
C’est une manière de désactiver le checking en disant qu’une variable peut être
“n’importe quoi”.
Le tooling
Imagine que tu veuilles faire une webapp. Tu te retrouves à installer Typescript, son tooling et comprendre sa configuration! Je dois admettre que c’est parfois le plus gros point de difficulté. La configuration de Typescript est une étape qui frustre énormément de gens et l’interaction avec des librairies externes est parfois difficile quand les types sont de mauvaise qualité. On a parfois l’impression de combattre le type system.
La verbosité
Tu passes d’un code simple et conçis, à un code annoté, qui demande parfois l’écriture manuelle de type. C’est en théorie plus de taf. Il faut voir ça comme un investissement que tu fais pour avoir moins de bugs et plus de facilités de maintenance. Un truc qui aide avec la verbosité c’est l’inférence, qui va finalement te permettre d’écrire moins d’annotations tout en te permettant de profiter des avantages cités.
Finalement Typescript ou pas?
Oui!
Les avantages surpassent tellement les inconvénients. J’ai fait le pari de Typescript en 2015 et je ne regrette pas du tout. D’un point de vue employabilité, ouverture d’esprit et progression de carrière, le langage m’a apporté beaucoup. C’est une - excellente - habitude à prendre qui peut ouvrir la porte vers d’autres choses dans le même style, pour le frontend:
Quelques ressources pour t’aider à démarrer: