Greboca  

Suport technique et veille technologique

Aujourd’hui, les grandes entreprises et administrations publiques hésitent entre continuer à utiliser des logiciels propriétaires ou basculer vers les Logiciels Libres. Pourtant, la plupart des logiciels libres sont capables de bien traiter les données issues des logiciels propriétaire, et parfois avec une meilleur compatibilité.

C’est alors la barrière de la prise en main qui fait peur, et pourtant...

Les logiciels libres

L’aspect « Logiciel Libre » permet une évolution rapide et une plus grande participation des utilisateurs. Les aides et tutoriels foisonnent sur Internet ou sont directement inclus dans le logiciel lui-même.

Enfin, les concepteurs sont plus proches des utilisateurs, ce qui rend les logiciels libres plus agréable à utiliser et conviviaux.

Grâce à la disponibilité des logiciels libres, vous trouverez facilement des services de support techniques et la licence n’est plus un frein à l’utilisation de ces logiciels par votre personnel.

Notre support technique concerne essentiellement les logiciels libres, que ce soit sous forme de services ponctuels ou de tutoriels.

LinuxFr.org : les journaux  -  Vérifiez vos types avec TypeScript et io-ts

 -  Septembre 2018 - 

Sommaire

TL,DR : avec TypeScript et io-ts, on peut passer d'un programme JavaScript court et correct, mais fragile et difficile à maintenir, à un programme facile à refactorer et robuste contre les modifications externes inattendues, rien qu'en ajoutant une définition de type et un if.

TL,DR bis : vous pouvez regarder le premier et le dernier exemple de code du journal pour vous faire une idée.

JavaScript c'est quoi ?

JavaScript est un langage au typage dynamique et faible.

Pour décrire la chose simplement (mais au risque de faire hurler les puristes), cela veut dire :

  • Typage dynamique : les types ne sont pas déclarés dans le code source (en JavaScript, on utilise des mots-clé comme "var" ou "let" et non des types comme "int")
  • Typage faible : il y a peu de vérifications des erreurs de type et il est possible de faire des cast implicites (en JavaScript on peut passer n'importe quel type à une fonction, et si le type n'est pas adapté on aura le plus souvent une erreur logique qu'une erreur de type)

Ces spécificités sont assez connues même en dehors des utilisateurs, grâce à un florilège d'exemples cocasses qui sont souvent la sources de moqueries sur le langage (personnellement, je trouve que ce n'est pas la faute du langage : si on saisit des expressions débiles, il ne faut pas être surpris d'avoir un résultat débile).

Cela dit, ces spécificités sont très pratiques pour faire du code concis et réutilisable.

Par exemple, admettons que l'ont ait une application qui utilise une API météo, on peut vouloir :

  • faire un appel à l'API en HTTP et traiter le résultat
  • utiliser un résultat de l'API qu'on a stocké de la base de donnée et le traiter
  • traiter un "résultat" écrit à la main (par exemple pour faire un test unitaire).

Pour faire plus concret, disons que l'API retourne un relevé sous forme de JSON de la sorte :

{
    "city":"Paris",
    "time":"2018-09-11T10:53:21Z",
    "temperature":14
}

Ensuite, on veut traiter le relevé via une fonction "traiteRelevé".

Dans un langage au typage fort et statique, on aurait besoin d'un objet ou d'une structure "relevé", et de convertir les trois sources (json de l'API, enregistrement de la base de données, expression dans le code) vers ce type, probablement de trois façons différentes.

En JavaScript, pas besoin de tout ça :

// 1) avec l'API
const fetch_res = await fetch("https://example.com/api/releve/Paris")
const releve1 = await fetch_res.json()

// 2) avec la base de données
const postgres_res = await client.query("select city,time,temperature from data where city='Paris'")
const releve2 = postgres_res.rows[0]

// 3) en dur
const releve3 = { city:"Paris", time:"2018-09-11T10:53:21Z", temperature:14 }

traiteReleve(releve1)
traiteReleve(releve2)
traiteReleve(releve3)

Les trois objets releve peuvent être utilisés interchangeablement, bien qu'ils viennent de sources différentes, et, si l'on peut faire confiance à la base de données et à l'API, on n'a même pas de code de vérification d'erreur supplémentaire à écrire.

Oui mais pourquoi tu nous parles de TypeScript et d'io-ts si JavaScript c'est aussi bien ?

Le code qu'on vient de voir est concis, marche bien (je suis modeste), mais il a un gros souci : il est très fragile et difficile à maintenir.

Parmi plein de choses, les choses suivantes peuvent arriver :

  • Même si on fait confiance à l'API (d'un point de vue sécurité), le type de données retournées peut être modifié
  • On peut modifier la requête sql, ou les types dans la base de données
  • On peut décider que c'est plus pratique, dans les objets relevé, d'avoir le champ "time" sous forme de Date et non de chaine de caractères.
  • On peut appeler la fonction relevé avec quelque chose qui n'est pas un relevé tel qu'on l'a prévu.

Pour tous ces problèmes on aura une erreur incompréhensible à l'exécution, voire, encore pire, pas d'erreur et un résultat faux à l'exécution.

Par exemple, supposons que la fonction traiteReleve retourne une chaine de caractères de type "14°C" et que le champ "temperature" est renommé en "temp".

// Cas 1 : erreur incompréhensible
const traiteReleve = (releve) => releve.temperature.concat("°C")
traiteReleve(releveAvecTemp)

// -> TypeError: undefined is not an object

// Cas 2 : mauvais résultat
const traiteReleve = (releve) => releve.temperature + "°C"
traiteReleve(releveAvecTemp)

// -> retourne la chaine "undefined°C"

Dans le 1er cas, avec un peu de chance on retrouvera l'erreur dans les logs, mais dans le 2e, encore moins de chance de voir le problème.

TypeScript à la rescousse

TypeScript est un système de types fort et statique, mais qui est conçu exprès pour JavaScript. Il sert uniquement à l'analyse statique (ie. vérifier le code et trouver les erreurs au moment de la compilation). Il a deux gros avantages :

  • il est conçu de façon a respecter "l'esprit" de JavaScript et permet de faire des patterns très dynamiques tels ceux qu'on a vu plus haut
  • il utilise ce qu'on appelle l'inférence de types : il déduit les types du contexte, et donc il y a très peu de types à déclarer explicitement.

D'ailleurs, si on reprend le code du début du journal, la version TypeScript est identique à la version JavaScript. Le revoilà avec en commentaire les types que TypeScript a trouvé :

// 1) avec l'API
const fetch_res = await fetch("https://example.com/api/releve/Paris") // type: Response
const releve1 = await fetch_res.json() // type: any

// 2) avec la base de données
const postgres_res = await client.query("select city,time,temperature from data where city='Paris'") // type: QueryResult
const releve2 = postgres_res.rows[0] // type: any

// 3) en dur
const releve3 = { city:"Paris", time:"2018-09-11T10:53:21Z", temperature:14 } // type: {city: string, time: string, temperature: number}

traiteReleve(releve1)
traiteReleve(releve2)
traiteReleve(releve3)

Par ailleurs, la définition de traiteRelevé est différente en TypeScript :

// En JS :
function traiteReleve(releve) {}
// En TypeScript :
function traiteReleve(releve: {city: string, time: string, temperature: number}) {}

Regardons de plus près ce que fait TypeScript pour notre code

Qu'est-ce que TypeScript a fait ?

Pour les cas 1) et 2), TypeScript a affecté le résultat de la requête HTTP et de la requête SQL au type any (logique, au moment de la compilation tout ce qu'on sait c'est qu'une requête http ou sql peut retourner n'importe quoi). Par contre, pour le cas 3), il a deviné tout seul le type de notre constante releve3.

Après, on appelle la fonction traiteReleve, dont le type du paramètre a été défini. Qu'est-ce qui va se passer ?

Cas 3

Pour le cas 3, TypeScript constate que le type attendu par la fonction est le même que celui de releve3, il est content.

Maintenant admettons qu'on modifie releve3 ainsi :

const releve3 = { city:"Paris", time:new Date("2018-09-11T10:53:21Z"), temperature:14 } // type: {city: string, time: string, temperature: number}

TypeScript va constater tout seul que time est de type Date dans releve3, mais que la fonction traiteReleve attend un time de type string: il y aura une erreur au moment de la compilation.

Dans un vrai programme, il y aura des erreurs à tous les endroits problématiques, ce qui rend la refactorisation très facile.

Cas 1 et 2

Dans les cas 1 et 2, TypeScript ne peut pas deviner le type des variables releve1 et releve2.

Les relevés sont de type "any". Le type "any" signifie que la variable de ce type est assignable à des variables de n'importe quel type. Ca signifie qu'il n'y a pas de vérification sur les types ce qui est assez nul.

Avec TypeScript 3.0, un nouveau type a été ajouté: "unknown". Ce type est le contraire de "any" : il signifie que la variable de type "unknown" est assignable à à peu près rien. Cela veut dire qu'on est obligé de faire une vérification ou un cast explicite pour continuer. Espérons que les types des librairies seront réécrits pour utiliser ce nouveau type.

Cela dit, dans les cas 1 et 2 et qu'on utilise unknown ou any, TypeScript ne peut pas nous aider : le type des variables retournés par les appels HTTP et à la base de données ne sont connus qu'à l'exécution.

On a deux solutions :

  • partir de l'hypothèse que la base de données et l'API retournent toujours les bons types et faire un cast
  • vérifier les types à l'exécution

On a dit au début du journal qu'on voulait rendre notre code moins fragile, donc c'est sur la vérification des types à l'exécution qu'on va partir.

La vérification des types à l'exécution (en général, pas que en JavaScript)

Quand on écrit un programme, l'idéal est de vérifier un maximum de choses à la compilation : on sait tout de suite qu'il y a une erreur, on n'a pas besoin de tester toutes les branches possibles pour la trouver.

Cependant, on ne peut pas tout vérifier à la compilation. Déjà, c'est impossible : l'analyse statique d'un programme est un problème indécidable. Mais surtout, pour un "vrai programme" on a des données qui viennent de l'extérieur qu'on ne contrôle pas.

La solution habituellement retenue, est d'utiliser un typage statique à l'intérieur du programme (donc de vérifier à la compilation), et de s'assurer aux entrées du programme que les données passées sont bien du type attendu (donc de vérifier à l'exécution)

La vérification des types à l'exécution (pour notre exemple)

On a dit au début du journal que l'avantage de JavaScript était que le code était court et concis, et que pour notre exemple il n'y a pas besoin de faire de vérifications. Mais maintenant on veut en faire. C'est contradictoire.

C'est là que la librairie io-ts vient à la rescousse. Elle permet de faire des vérifications à l'exécution de façon extrêmement simple et concise. Regardons comment l'utiliser.

io-ts

Dans notre exemple, on veut s'assurer que ce qui est retourné par l'API ou HTTP ressemble bien à un referer.

Pour cela on va définir un "type" io-ts :

const Releve = t.type({
  city: t.string,
  time: t.string,
  temperature: t.number
})

Si vous connaissez JavaScript, vous vous direz "hmm, c'est probablement un objet, pas un type". Effectivement, c'est un objet, et voyons maintenant comment l'utiliser :

const Releve = t.type({
  city: t.string,
  time: t.string,
  temperature: t.number
})

const releve: unknown = await fetch_res.json() // on caste releve à unknown pour éviter qu'il soit utilisé sans qu'on fasse de vérification

// releve est de type unknown

if (Releve.is(releve)) {
    // releve est désormais de type {city: string, time: string, temperature: number}
    traiteReleve(releve)
} else {
    // et là releve est de nouveau de type unknown
    throw "Erreur"
}

On n'a fait qu'ajouter un "type" et un if, mais il s'est passé plein de choses :

  • statiquement (au moment de la compilation) :
    1. Grâce à l'inférence de types, TypeScript sait que Releve est d'un certain type, défini à partir des types de t.type, t.string, etc. (t.* sont des fonctions et propriétés d'io-ts).
    2. Toujours grâce à l'inférence de types, TypeScript sait Releve.is est une fonction de type typeguard (elle vérifie que son argument est d'un certain type).
    3. Enfin, TypeScript déduit que releve est de type {city: string, time: string, temperature: number} dans la branche principale du if mais de type unknown dans l'autre.
  • dynamiquement (au moment de l'exécution) : la fonction Releve.is est exécutée et vérifie si releve est du type attendu

En résumé, en ajoutant quelques lignes qui ne sont pas plus longues qu'une déclaration de struct en C ou d'object en Java :
- On a une erreur à la compilation si on utilise la variable releve obtenue à partir de l'API sans l'avoir vérifiée
- On a à l'exécution une fonction qui vérifie le type de l'objet obtenu à partir de l'API

En conclusion :

Voilà le programme du début complété avec tout ce qu'on a vu :

const Releve = t.type({
  city: t.string,
  time: t.string,
  temperature: t.number
})

// 1) avec l'API
const fetch_res = await fetch("https://example.com/api/releve/Paris")
const releve1:unknown = await fetch_res.json()

// 2) avec la base de données
const postgres_res = await client.query("select city,time,temperature from data where city='Paris'")
const releve2:unknown = postgres_res.rows[0]

// 3) en dur
const releve3 = { city:"Paris", time:"2018-09-11T10:53:21Z", temperature:14 }

if (Releve.is(releve1) && Releve.is(releve2)) {
    traiteReleve(releve1)
    traiteReleve(releve2)
    traiteReleve(releve3)
}
else
    gererLErreur()

Avec un if et une "déclaration de type" en plus, on a une gestion complète des types, que ce soit à la compilation ou à l'exécution, et on n'aura pas de surprise, ni quand on modifiera notre programme, ni quand les services externes (bdd, api) sont modifié.

Commentaires : voir le flux atom ouvrir dans le navigateur

par n_e

LinuxFr.org : les journaux

LinuxFr.org : Journaux

Téléphone sous Linux ?

 -  25 avril - 

Aujourd'hui, avoir un téléphone avec un Android libéré, c'est possible, on pense en particulier à Murena.Avoir un téléphone sous GNU/Linux, c'est (...)


Quand votre voiture vous espionne… et vous le fait payer

 -  23 avril - 

Ceci se passe aux États-Unis, pour l’instant, aucune preuve qu’une telle fuite existe en Europe. Mais… si votre assurance augmente brutalement, (...)


firefox, nouvelle fenêtre dans une session isolée

 -  15 avril - 

Les fenêtres de navigation privées de firefox partagent leurs cookies de session or je souhaitais avoir des fenêtres de navigation isolées, (qui ne (...)


Pretendo tente de déprogrammer l'obsolescence des consoles Nintendo

 -  9 avril - 

Ah Nal,Gros N vient de faire un gros doigt aux utilisateurs de ses consoles 3DS et Wii U en annonçant la fermeture des services en ligne pour (...)


[Trolldi] Vulgarisation sur l'IA pour décideur pressé

 -  5 avril - 

Cher 'Nal,Je fais un article-marque-page sur un post tout frais de Ploum où il est question d'un fantasme vieux comme le Talmud avec le Golem. (...)