TypeScript : 10 idées pour mieux gérer les erreurs liées au typage !

Aujourd’hui, TypeScript a le vent en poupe grâce à la consistance et la cohérence que le typage permet d’apporter au code JavaScript. Toutefois, il n’est pas si simple d’exploiter pleinement le potentiel de TypeScript et de son typage.

En effet, une mauvaise implémentation de votre typage et des règles qui lui sont associées entraînent une non-détection des erreurs par le compilateur ainsi qu’un code complexe, difficilement maintenable et refactorable.

Dans cet article, le cofondateur de TheCodingMachine, David Négrier, vous donne ses astuces et outils pour exploiter pleinement le potentiel du typage et éviter les erreurs.

Bref, facilitez-vous la vie avec un code simple, bien construit et facile à retravailler !

Typer, mais pourquoi faire ?

  • Éviter les régressions: en cas de changements du code (refactoring, correction de bugs, ajout de fonctionnalité etc…), le compilateur vous alertera des incohérences ou oublis
  • Gagner du temps: grâce au typage l’IDE est plus pertinent dans ses suggestions d’autocomplétion.

Les outils et les solutions qu’on vous propose

TS-reset

Un paquet qui prend tous les typages codés par défaut (JSON.parse(), …) et remplace le typage any par un typage unknown.

Ceci permet d’être plus stricte dans l’analyse des types et donc remonter une erreur sur ce type de code:

const user = JSON.parse('{"id": 1, "firstName": "John"}')
console.log(user.firstName) // 'user' is of type 'unknown'.

Sans ts-reset aucune erreur ne sera remontée car tout est autorisé sur any. Alors qu’avec le développeur est forcé de vérifier le typage:

const user = JSON.parse('{"id": 1, "firstName": "John"}')
if (
  user instanceof Object
  && 'firstName' in user
  && typeof user.firstName === 'string'
) {
  console.log(user.firstName) // 'user.firstName' is a string.
}

– Créer une interface

Il est important de créer des interfaces pour définir la structure d’un objet avec ses différents champs.

interface User {
  id: number;
  firstName: string;
}

// On ajoute `as User` pour indiquer que cette variable est de type User
const user = JSON.parse('{"id": 1, "firstName": "John"}') as User
console.log(user.firstName) // Affiche "John"

Mais que se passe-t-il si le format change dans le JSON d’origine ?

Par exemple, si on remplace firstName par first_name dans le fichier JSON, un bug est créé et on retrouve ”undefined” dans nos logs (sans message d’erreur). 

Cela est dû au fait que la vérification ne s’effectue pas lors de l’exécution, mais uniquement lors de la compilation. Les interfaces disparaissent dans le code compilé.

L’erreur provient de l’utilisation de as User, le mot clé as en TypeScript permet de définir un “Type Assertion” et donc de forcer, du point de vue du compilateur, le type d’une variable. 
Si le type est faux, l’analyse faite par TypeScript devient incorrecte. L’erreur est humaine et le compilateur a tendance à faire moins d’erreurs. En particulier, lorsque une application complexe évolue au cours du temps. Donc un as dans TypeScript doit toujours être un sujet de vigilance !

– Utiliser les Type predicate

Afin d’éviter le as il faut restreindre (narrow) le type. Une solution lorsqu’on a une interface est d’utiliser un Type predicate:

interface User {
  id: number;
  firstName: string;
}

// On définit un type predicate de la forme `parameterName is Type` 
function isUser(user: any): user is User {
  return user
    && typeof user.id === 'number'
    && typeof user.firstName === 'string'
}

const user = JSON.parse('{"id": 1, "firstName": "John"}')
if (! isUser(user)) { // On appelle le predicate
  throw new Error('Invalid user')
}
console.log(user.firstName) // Ici on est sûr d'avoir un User

Avec ce code, si firstName devient first_name on aura une erreur “Invalid user” au runtime. En effet, le type predicate est conservé et exécuté.

Toutefois, implémenter des types predicate c’est fastidieux et repose toujours sur la responsabilité du développeur qui doit l’implémenter dans son code de manière proactive. 

Il faut aussi être vigilant lors de l’écriture du type predicate pour ne pas faire d’erreur à ce moment-là et bien penser à le mettre à jour lorsqu’on modifie le type lié.

– La librairie Zod

Zod est une libraire qui écrit des types predicate pour nous.
Grâce à elle nous pouvons remplacer le type predicate isUser et définir une variable UserSchema qui est un objet zod:

interface User {
  id: number;
  firstName: string;
}

const UserSchema = z.object({
  id: z.number(),
  firstName: z.string(),
})

const maybeUser = JSON.parse(...)
const user = UserSchema.parse(maybeUser)
// À partir d'ici user est de type `{ id: number, firstName: string }

Zod peut aussi déduire l’interface à partir du Type Guard et on peut donc se passer d’écrire l’interface :

type User = z.infer<typeof UserSchema>

Lors de l’exécution, Zod affichera une erreur détaillé s’il y a un soucis:

ZodError: [{
  "code": "invalid_type",
  "expected": "string",
  "received": "undefined",
  "path": ["firstName"],
  "message": "Required",
}]

Zod est donc particulièrement utile : 

  • Dans les projets Front qui utilisent des inputs venant de l’extérieur, en particulier d’API.
  • dans les projets Back qui utilisent Node.JS, en cas d’input HTTP, d’upload de fichier, d’input CLI ou de réponse d’API, …

Il est néanmoins inutile d’utiliser Zod pour un service qui utilise déjà des données validées (données en provenance d’un Controller par exemple). Dans ce cas, les interfaces peuvent suffir… ou alors il est possible d’utiliser l’inférence propre à TypeScript.
Une autre librairie similaire est valibot.

– Type Inference

TypeScript fait de l’inférence de type. C’est-à-dire qu’en analysant les contraintes appliquées à chaque variable il peut en déduire son type. Ceci évite de devoir tout typer tout le temps :

const a = 1 // a est de type number
const b = [0, 1, null] // b est de type (number | null)[]
const c = a // c est de type number
const d = { id: 1, firstName: "John" } // d est de type { id: number; firstName: string; }
window.onmousedown = function (e) { /* ... */ }; // e est de type MouseEvent

Il est donc possible de se passer d’interface la plupart du temps. Nous recommandons fortement de s’appuyer sur l’inférence de type et utiliser Zod  là où c’est nécessaire pour gagner en efficacité et en maintenabilité de votre code !

GraphQL et le paquet graphql-codegen

L’utilisation de Zod et TypeScript peut être synonyme de verbosité. En effet, pour être correcte nous devrions ré-écrire les structures des objets renvoyés par l’api dans notre code front. Le travail est fait 2 fois: en back et en front.

Contrairement à du JSON, GraphQL définit un schéma, or cela va typer l’API correctement.

Le paquet graphql-codegen est un outil qui va scanner votre code TypeScript et va trouver les requêtes GraphQL à l’intérieur. 

Puis, à partir de ces requêtes et du schéma généré par le serveur, graphql-codegen va générer automatiquement des types TypeScript. 

Néanmoins, mettre en place graphql-codegen nécessite un peu de travail notamment en CI/CD.

– Swagger/OpenAPI (via API Platform)

Dans la même idée, certains framework (type API Platform) permettent de générer les interfaces typescript en fonction des retours des endpoints REST.

Il existe aussi des générateurs pour les standards Swagger/OpenAPI.

– Framework trpc

Il s’agit d’un framework Node.Js dans lequel on définit côté serveur une procédure qui prend un type Zod en input puis côté client on appelle la fonction ce qui permet un typage fort des deux côtés.

Il s’agit de faire des appels RPC entre le serveur et le client.
Hono propose aussi ce genre de fonctionnalité.

Protobuf

Solution plus bas niveau, il s’agit d’un mécanisme de Serialization, un peu comme du JSON mais fortement typé.

On définit des types Protobuf qui génèrent des interfaces TypeScript.

Particulièrement utile pour optimiser les performances car les messages sont compressés de manière binaire.

– No Front, l’approche JS Less

Solution la plus radicale, mais sans Front, la question ne se pose plus ! On appelle cela le JS Less et de nombreuses solutions existent, vous pouvez lire notre article sur le sujet.


par TheCodM

Articles similaires TAG