Si vous créez des applications Web complexes, TypeScript est probablement votre langage de programmation de choix. TypeScript est très apprécié pour son système de typage puissant et ses capacités d'analyse statique, ce qui en fait un outil puissant pour garantir que votre code est robuste et sans erreur.
Il accélère également le processus de développement grâce à l'intégration avec les éditeurs de code, permettant aux développeurs de naviguer plus efficacement dans le code et d'obtenir des conseils et une auto-complétion plus précis, tout en permettant une refactorisation sûre de grandes quantités de code.
Le compilateur est le cœur de TypeScript, responsable de la vérification de l'exactitude du type et de la transformation du code TypeScript en JavaScript. Cependant, pour utiliser pleinement la puissance de TypeScript, il est important de configurer correctement le compilateur.
Chaque projet TypeScript possède un ou plusieurs fichiers tsconfig.json
qui contiennent toutes les options de configuration du compilateur.
La configuration de tsconfig est une étape cruciale pour obtenir une sécurité de type et une expérience de développement optimales dans vos projets TypeScript. En prenant le temps d'examiner attentivement tous les facteurs clés impliqués, vous pouvez accélérer le processus de développement et vous assurer que votre code est robuste et sans erreur.
La configuration par défaut dans tsconfig peut faire perdre aux développeurs la majorité des avantages de TypeScript. En effet, il ne permet pas de nombreuses fonctionnalités puissantes de vérification de type. Par configuration "par défaut", j'entends une configuration dans laquelle aucune option du compilateur de vérification de type n'est définie.
Par exemple:
{ "compilerOptions": { "target": "esnext", "module": "esnext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, }, "include": ["src"] }
L'absence de plusieurs options de configuration clés peut entraîner une qualité de code inférieure pour deux raisons principales. Premièrement, le compilateur de TypeScript peut gérer de manière incorrecte les types null
et undefined
dans divers cas.
Deuxièmement, any
type peut apparaître de manière incontrôlable dans votre base de code, entraînant une vérification de type désactivée autour de ce type.
Heureusement, ces problèmes sont faciles à résoudre en modifiant quelques options dans la configuration.
{ "compilerOptions": { "strict": true } }
Le mode strict est une option de configuration essentielle qui offre des garanties plus solides d'exactitude du programme en permettant un large éventail de comportements de vérification de type.
L'activation du mode strict dans le fichier tsconfig est une étape cruciale pour atteindre une sécurité de type maximale et une meilleure expérience de développement.
Cela nécessite un petit effort supplémentaire pour configurer tsconfig, mais cela peut grandement améliorer la qualité de votre projet.
L'option de compilateur strict
active toutes les options de la famille de mode strict, qui incluent noImplicitAny
, strictNullChecks
, strictFunctionTypes
, entre autres.
Ces options peuvent également être configurées séparément, mais il n'est pas recommandé de les désactiver. Regardons des exemples pour voir pourquoi.
{ "compilerOptions": { "noImplicitAny": true } }
Le type any
est une faille dangereuse dans le système de type statique, et son utilisation désactive toutes les règles de vérification de type. En conséquence, tous les avantages de TypeScript sont perdus : des bogues sont manqués, les conseils de l'éditeur de code cessent de fonctionner correctement, etc.
L'utilisation any
n'est acceptable que dans des cas extrêmes ou pour des besoins de prototypage. Malgré tous nos efforts, any
type peut parfois se faufiler implicitement dans une base de code.
Par défaut, le compilateur nous pardonne beaucoup d'erreurs en échange de l'apparition de any
dans une base de code. Plus précisément, TypeScript nous permet de ne pas spécifier le type des variables, même lorsque le type ne peut pas être déduit automatiquement.
Le problème est que nous pouvons accidentellement oublier de spécifier le type d'une variable, par exemple, à un argument de fonction. Au lieu d'afficher une erreur, TypeScript déduira automatiquement le type de la variable comme any
.
function parse(str) { // ^? any return str.split(''); } // TypeError: str.split is not a function const res1 = parse(42); const res2 = parse('hello'); // ^? any
L'activation de l'option de compilateur noImplicitAny
amènera le compilateur à mettre en surbrillance tous les endroits où le type d'une variable est automatiquement déduit comme any
. Dans notre exemple, TypeScript nous demandera de spécifier le type de l'argument de la fonction.
function parse(str) { // ^ Error: Parameter 'str' implicitly has an 'any' type. return str.split(''); }
Lorsque nous spécifions le type, TypeScript détectera rapidement l'erreur de transmission d'un nombre à un paramètre de chaîne. La valeur de retour de la fonction, stockée dans la variable res2
, aura également le type correct.
function parse(str: string) { return str.split(''); } const res1 = parse(42); // ^ Error: Argument of type 'number' is not // assignable to parameter of type 'string' const res2 = parse('hello'); // ^? string[]
{ "compilerOptions": { "useUnknownInCatchVariables": true } }
La configuration de useUnknownInCatchVariables
permet une gestion sûre des exceptions dans les blocs try-catch. Par défaut, TypeScript suppose que le type d'erreur dans un bloc catch est any
, ce qui nous permet de faire n'importe quoi avec l'erreur.
Par exemple, nous pourrions transmettre l'erreur interceptée telle quelle à une fonction de journalisation qui accepte une instance de Error
.
function logError(err: Error) { // ... } try { return JSON.parse(userInput); } catch (err) { // ^? any logError(err); }
Cependant, en réalité, il n'y a aucune garantie sur le type d'erreur, et nous ne pouvons déterminer son vrai type qu'au moment de l'exécution lorsque l'erreur se produit. Si la fonction de journalisation reçoit quelque chose qui n'est pas une Error
, cela entraînera une erreur d'exécution.
Par conséquent, l'option useUnknownInCatchVariables
fait passer le type d'erreur de any
à unknown
pour nous rappeler de vérifier le type d'erreur avant de faire quoi que ce soit avec.
try { return JSON.parse(userInput); } catch (err) { // ^? unknown // Now we need to check the type of the value if (err instanceof Error) { logError(err); } else { logError(new Error('Unknown Error')); } }
Maintenant, TypeScript nous demandera de vérifier le type de la variable err
avant de la transmettre à la fonction logError
, ce qui donnera un code plus correct et plus sûr. Malheureusement, cette option ne résout pas les erreurs de frappe dans les fonctions promise.catch()
ou les fonctions de rappel.
Mais nous discuterons des moyens de faire face à any
tels cas dans le prochain article.
{ "compilerOptions": { "strictBindCallApply": true } }
Une autre option corrige l'apparence de any
les appels en fonction via call
and apply
. C'est un cas moins courant que les deux premiers, mais il est toujours important de le considérer. Par défaut, TypeScript ne vérifie pas du tout les types dans de telles constructions.
Par exemple, nous pouvons passer n'importe quoi comme argument à une fonction, et à la fin, nous recevrons toujours le type any
.
function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? any const n2 = parse.call(undefined, false); // ^? any
L'activation de l'option strictBindCallApply
rend TypeScript plus intelligent, de sorte que le type de retour sera correctement déduit en tant que number
. Et lorsque vous essayez de passer un argument du mauvais type, TypeScript pointera vers l'erreur.
function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? number const n2 = parse.call(undefined, false); // ^ Argument of type 'boolean' is not // assignable to parameter of type 'string'.
{ "compilerOptions": { "noImplicitThis": true } }
L'option suivante qui peut aider à empêcher l'apparition de any
dans votre projet corrige la gestion du contexte d'exécution dans les appels de fonction. La nature dynamique de JavaScript rend difficile la détermination statique du type de contexte à l'intérieur d'une fonction.
Par défaut, TypeScript utilise le type any
pour le contexte dans de tels cas et ne fournit aucun avertissement.
class Person { private name: string; constructor(name: string) { this.name = name; } getName() { return function () { return this.name; // ^ 'this' implicitly has type 'any' because // it does not have a type annotation. }; } }
L'activation de l'option de compilateur noImplicitThis
nous invitera à spécifier explicitement le type de contexte pour une fonction. De cette façon, dans l'exemple ci-dessus, nous pouvons détecter l'erreur d'accès au contexte de la fonction au lieu du champ name
de la classe Person
.
{ "compilerOptions": { "strictNullChecks": true } }
Ensuite, plusieurs options incluses dans le mode strict
n'entraînent pas l'apparition de any
type dans la base de code. Cependant, ils rendent le comportement du compilateur TS plus strict et permettent de trouver plus d'erreurs lors du développement.
La première de ces options corrige la gestion de null
et undefined
dans TypeScript. Par défaut, TypeScript suppose que null
et undefined
sont des valeurs valides pour n'importe quel type, ce qui peut entraîner des erreurs d'exécution inattendues.
L'activation de l'option de compilateur strictNullChecks
oblige le développeur à gérer explicitement les cas où null
et undefined
peuvent se produire.
Par exemple, considérez le code suivant :
const users = [ { name: 'Oby', age: 12 }, { name: 'Heera', age: 32 }, ]; const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } console.log(loggedInUser.age); // ^ TypeError: Cannot read properties of undefined
Ce code se compilera sans erreur, mais il peut générer une erreur d'exécution si l'utilisateur portant le nom "Max" n'existe pas dans le système et que users.find()
renvoie undefined
. Pour éviter cela, nous pouvons activer l'option de compilateur strictNullChecks
.
Maintenant, TypeScript nous forcera à gérer explicitement la possibilité que null
ou undefined
soit renvoyé par users.find()
.
const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } | undefined if (loggedInUser) { console.log(loggedInUser.age); }
En gérant explicitement la possibilité de null
et undefiined
, nous pouvons éviter les erreurs d'exécution et nous assurer que notre code est plus robuste et sans erreur.
{ "compilerOptions": { "strictFunctionTypes": true } }
L'activation strictFunctionTypes
rend le compilateur de TypeScript plus intelligent. Avant la version 2.6, TypeScript ne vérifiait pas la contravariance des arguments de fonction. Cela conduira à des erreurs d'exécution si la fonction est appelée avec un argument du mauvais type.
Par exemple, même si un type de fonction est capable de gérer à la fois des chaînes et des nombres, nous pouvons affecter une fonction à ce type qui ne peut gérer que des chaînes. Nous pouvons toujours passer un nombre à cette fonction, mais nous recevrons une erreur d'exécution.
function greet(x: string) { console.log("Hello, " + x.toLowerCase()); } type StringOrNumberFn = (y: string | number) => void; // Incorrect Assignment const func: StringOrNumberFn = greet; // TypeError: x.toLowerCase is not a function func(10);
Heureusement, l'activation de l'option strictFunctionTypes
corrige ce comportement, et le compilateur peut détecter ces erreurs au moment de la compilation, nous montrant un message détaillé de l'incompatibilité de type dans les fonctions.
const func: StringOrNumberFn = greet; // ^ Type '(x: string) => void' is not assignable to type 'StringOrNumberFn'. // Types of parameters 'x' and 'y' are incompatible. // Type 'string | number' is not assignable to type 'string'. // Type 'number' is not assignable to type 'string'.
{ "compilerOptions": { "strictPropertyInitialization": true } }
Enfin, l'option strictPropertyInitialization
permet de vérifier l'initialisation obligatoire des propriétés de classe pour les types qui n'incluent pas undefined
comme valeur.
Par exemple, dans le code suivant, le développeur a oublié d'initialiser la propriété email
. Par défaut, TypeScript ne détecterait pas cette erreur et un problème pourrait survenir lors de l'exécution.
class UserAccount { name: string; email: string; constructor(name: string) { this.name = name; // Forgot to assign a value to this.email } }
Cependant, lorsque l'option strictPropertyInitialization
est activée, TypeScript mettra en évidence ce problème pour nous.
email: string; // ^ Error: Property 'email' has no initializer and // is not definitely assigned in the constructor.
{ "compilerOptions": { "noUncheckedIndexedAccess": true } }
L'option noUncheckedIndexedAccess
ne fait pas partie du mode strict
, mais c'est une autre option qui peut aider à améliorer la qualité du code dans votre projet. Il permet de vérifier que les expressions d'accès à l'index ont un type de retour null
ou undefined
, ce qui peut éviter les erreurs d'exécution.
Considérez l'exemple suivant, où nous avons un objet pour stocker les valeurs mises en cache. Nous obtenons alors la valeur de l'une des clés. Bien sûr, nous n'avons aucune garantie que la valeur de la clé souhaitée existe réellement dans le cache.
Par défaut, TypeScript supposerait que la valeur existe et a le type string
. Cela peut entraîner une erreur d'exécution.
const cache: Record<string, string> = {}; const value = cache['key']; // ^? string console.log(value.toUpperCase()); // ^ TypeError: Cannot read properties of undefined
L'activation de l'option noUncheckedIndexedAccess
dans TypeScript nécessite de vérifier les expressions d'accès à l'index pour le type de retour undefined
, ce qui peut nous aider à éviter les erreurs d'exécution. Cela s'applique également à l'accès aux éléments d'un tableau.
const cache: Record<string, string> = {}; const value = cache['key']; // ^? string | undefined if (value) { console.log(value.toUpperCase()); }
En fonction des options décrites, il est fortement recommandé d'activer les options strict
et noUncheckedIndexedAccess
dans le fichier tsconfig.json
de votre projet pour une sécurité de type optimale.
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, } }
Si vous avez déjà activé l'option strict
, vous pouvez envisager de supprimer les options suivantes pour éviter de dupliquer l'option strict: true
:
noImplicitAny
useUnknownInCatchVariables
strictBindCallApply
noImplicitThis
strictFunctionTypes
strictNullChecks
strictPropertyInitialization
Il est également recommandé de supprimer les options suivantes qui peuvent affaiblir le système de type ou provoquer des erreurs d'exécution :
keyofStringsOnly
noStrictGenericChecks
suppressImplicitAnyIndexErrors
suppressExcessPropertyErrors
En examinant et en configurant soigneusement ces options, vous pouvez obtenir une sécurité de type optimale et une meilleure expérience de développement dans vos projets TypeScript.
TypeScript a parcouru un long chemin dans son évolution, améliorant constamment son compilateur et son système de typage. Cependant, pour maintenir la rétrocompatibilité, la configuration de TypeScript est devenue plus complexe, avec de nombreuses options qui peuvent affecter considérablement la qualité de la vérification de type.
En examinant et en configurant soigneusement ces options, vous pouvez obtenir une sécurité de type optimale et une meilleure expérience de développement dans vos projets TypeScript. Il est important de savoir quelles options activer et supprimer d'une configuration de projet.
Comprendre les conséquences de la désactivation de certaines options vous permettra de prendre des décisions éclairées pour chacune d'entre elles.
Il est important de garder à l'esprit qu'un typage strict peut avoir des conséquences. Pour gérer efficacement la nature dynamique de JavaScript, vous devrez avoir une bonne compréhension de TypeScript au-delà de la simple spécification de "nombre" ou "chaîne" après une variable.
Vous devrez vous familiariser avec des constructions plus complexes et l'écosystème de bibliothèques et d'outils TypeScript-first pour résoudre plus efficacement les problèmes liés au type qui se poseront.
Par conséquent, écrire du code peut nécessiter un peu plus d'efforts, mais d'après mon expérience, cet effort en vaut la peine pour les projets à long terme.
J'espère que vous avez appris quelque chose de nouveau grâce à cet article. Ceci est la première partie d'une série. Dans le prochain article, nous verrons comment améliorer la sécurité des types et la qualité du code en améliorant les types de la bibliothèque standard de TypeScript. Restez à l'écoute et merci d'avoir lu !