Article écrit par Jérôme Wohlschlegel
Typescript est arrivé dans l’écosystème Javascript comme étant la solution de typage que l’on attendait depuis très longtemps. Le fait de pouvoir typer son code permet d’avoir un code plus sécurisé, maintenable et d’éviter un grand nombre de bugs, mais le cas de Typescript est un peu particulier.
Prenons comme exemple la récupération de données via une API :
interface FactInterface {
text: string,
type: string,
user: string,
source: string,
used: boolean,
status: FactVerifInterface,
deleted: boolean,
createdAt: string,
updatedAt: string,
}
interface FactVerifInterface {
sentCount: number,
verified: boolean,
}
async function fetchApi(): Promise<FactInterface[]> {
const myApi = await fetch('https://cat-fact.herokuapp.com/facts');
return await myApi.json();
}
(async () => {
const facts = await fetchApi();
console.log(facts);
})();
Dans notre cas, nous récupérons des informations via une API JSON. Les informations sont très simples à comprendre.
Maintenant, nous voulons afficher les données et pour que cela soit plus agréable à lire, nous voulons mettre les dates de création et de mise à jour au format français. La documentation et des articles sur Internet nous conseillent d’écrire des solutions comme celle-ci :
import { DateTime } from "https://esm.sh/luxon";
interface FactInterface {
text: string,
type: string,
user: string,
source: string,
used: boolean,
status: FactVerifInterface,
deleted: boolean,
createdAt: DateTime, // Cast string to Luxon
updatedAt: DateTime, // Cast string to Luxon
}
interface FactVerifInterface {
sentCount: number,
verified: boolean,
}
async function fetchApi(): Promise<FactInterface[]> {
const myApi = await fetch('https://cat-fact.herokuapp.com/facts');
return await myApi.json();
}
(async () => {
const facts = await fetchApi();
for (const fact of facts) {
console.log(fact.createdAt.setLocale('fr').toLocaleString(DateTime.DATETIME_FULL)); // Error
}
})();
Ou bien avec l’assertion de type as
(https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions). Ce qui nous donnerait le code suivant :
fact.createdAt as DateTime; // Cast string to Luxon
fact.updatedAt as DateTime; // Cast string to Luxon
Une fausse idée est que Typescript “cast” les valeurs, donc en précisant le type de valeur que l’on veut, Typescript se chargerait de le changer lui-même. Malheureusement, comparé à d’autres langages de programmation, il ne fera pas ce travail.
Ce qu’il faut comprendre, c’est qu’actuellement Typescript n’est pas utilisé de manière “native”, le code est transpilé en Javascript et donc certaines fonctionnalités qui pourraient être utiles comme le casting ne sont pas pleinement disponibles. Il ne va pas changer une variable de type string
en number
.
Ces méthodes de casting ne sont là que pour préciser qu’une variable est dans un type plus précis, dans le cas où Typescript n’a pas réussi à détecter le bon. Il est intéressant de comprendre que si l’on utilise Typescript sur un projet, le typage est essentiellement pour que notre IDE comprenne bien le code du projet. Nous utilisons le typage pour nous aider nous-mêmes et avec l’aide de nos éditeurs de code, mais ça ne garantira pas une sécurité totale si nous typons mal notre code. Typescript est un outil de développement, le code ne sera pas exactement le même en production.
Dans notre cas, nous aurons très rapidement une erreur, car nous voulons l’utiliser immédiatement, mais sur des applications plus complexes où nous créons le modèle de données que nous n’utilisons pas tout de suite, il pourrait y avoir des erreurs plus importantes.
Ce problème vient la plupart du temps quand on utilise des API, donc des données qui sont externes à notre application. Pour corriger ça, il faut “parser” soi-même les données en faisant ce qu’on appelle la “deserialization” :
import { DateTime } from "https://esm.sh/luxon";
interface FactInterface {
text: string,
type: string,
user: string,
source: string,
used: boolean,
status: FactVerifInterface,
deleted: boolean,
createdAt: DateTime,
updatedAt: DateTime,
}
interface FactVerifInterface {
sentCount: number,
verified: boolean,
}
async function fetchApi(): Promise<FactInterface[]> {
const myApi = await fetch('https://cat-fact.herokuapp.com/facts');
const facts = await myApi.json();
return facts.map(fact => { // deserialization
fact.createdAt = new DateTime(fact.createdAt);
fact.updatedAt = new DateTime(fact.updatedAt);
return fact;
})
}
(async () => {
const facts = await fetchApi();
for (const fact of facts) {
console.log(fact.createdAt.setLocale('fr').toLocaleString(DateTime.DATETIME_FULL)); // "29 avril 2024 à 17:25 UTC+2"
}
})();
Il est possible de le rendre plus maintenable ou moins redondant en utilisant les génériques de Typescript ou bien en créant une entité qui proposerait directement de désérialiser les données, mais l’idée principale est là.
La question est pourquoi Typescript ne propose pas de caster comme dans d’autres langages ? Typescript est une très grande avancée dans l’évolution des applications web. Il propose énormément de fonctionnalités nécessaires pour l’avenir de l’écosystème JavaScript. Mais je pense qu’il y aura un nouveau changement quand nos environnements nous donneront la possibilité de faire du Typescript nativement et sans transpilation (comme Deno) et qu’ils seront bien plus adoptés. Quand ce cap sera franchi, des mises à jour importantes de casting seront proposées. En attendant, il serait intéressant de se tourner vers la solution de Zod qui permet même d’aller plus loin (https://zod.dev/).