Explorez les types littéraux de modèles TypeScript et construisez un moteur de validation d'exécution pour une vérification robuste des chaînes et la sécurité des types.
Moteur de Validation des Littéraux Modèles TypeScript : Vérification des Chaînes d’Exécution
Les types littéraux de modèles TypeScript offrent une manipulation de chaînes et une sécurité des types puissantes au moment de la compilation. Cependant, ces vérifications sont limitées au moment de la compilation. Cet article de blog explore comment construire un moteur de validation d'exécution pour les types littéraux de modèles TypeScript, permettant une vérification robuste des chaînes et prévenant les erreurs potentielles pendant l'exécution du programme.
Introduction aux Types Littéraux de Modèles TypeScript
Les types littéraux de modèles vous permettent de définir des formes de chaînes spécifiques basées sur des valeurs littérales, des unions et l'inférence de type. Cela permet une vérification de type précise et une auto-complétion, particulièrement utile lorsqu'il s'agit de données structurées ou de langages spécifiques au domaine.
Par exemple, considérez un type pour représenter les codes de devise :
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
const validCurrency: FormattedCurrencyString = "USD-100"; // OK
const invalidCurrency: FormattedCurrencyString = "CAD-50"; // Erreur de type au moment de la compilation
Cet exemple démontre comment TypeScript applique le type FormattedCurrencyString au moment de la compilation. Cependant, si le code de devise provient d'une source externe (par exemple, une entrée utilisateur, une réponse API), vous avez besoin d'une validation d'exécution pour garantir la sécurité des types.
La Nécessité de la Validation d'Exécution
Bien que TypeScript fournisse une excellente vérification de type au moment de la compilation, il ne peut pas garantir la validité des données reçues de sources externes au moment de l'exécution. Se fier uniquement aux types au moment de la compilation peut entraîner des erreurs et des vulnérabilités inattendues.
Considérez le scénario suivant :
function processCurrency(currencyString: FormattedCurrencyString) {
// ... une certaine logique qui suppose que la chaîne est correctement formatée
}
const userInput = "CAD-50"; // Supposons que cela provienne de l'entrée utilisateur
// Cela compilera, mais provoquera une erreur d'exécution si la logique à l'intérieur
// `processCurrency` repose sur le format.
processCurrency(userInput as FormattedCurrencyString);
Dans ce cas, nous convertissons userInput en FormattedCurrencyString, contournant les vérifications de TypeScript au moment de la compilation. Si processCurrency repose sur le fait que la chaîne soit correctement formatée, il rencontrera une erreur d'exécution.
La validation d'exécution comble cette lacune en vérifiant que les données reçues au moment de l'exécution sont conformes aux types TypeScript attendus.
Construire un Moteur de Validation des Littéraux Modèles
Nous pouvons construire un moteur de validation d'exécution en utilisant des expressions régulières et le système de types de TypeScript. Le moteur prendra un type littéral de modèle et une chaîne en entrée et renverra si la chaîne correspond au type.
Étape 1 : Définir un Type pour la Validation d'Exécution
Tout d'abord, nous avons besoin d'un type générique qui peut représenter l'équivalent d'exécution d'un type littéral de modèle. Ce type devrait être capable de gérer différents types de littéraux de modèles, y compris les littéraux, les unions et les paramètres de type.
type TemplateLiteralToRegex =
T extends `${infer Start}${infer Middle}${infer End}`
? Start extends string
? Middle extends string
? End extends string
? TemplateLiteralToRegexStart & TemplateLiteralToRegexMiddle & TemplateLiteralToRegex
: never
: never
: never
: TemplateLiteralToRegexStart;
type TemplateLiteralToRegexStart = T extends `${infer Literal}` ? Literal : string;
type TemplateLiteralToRegexMiddle = T extends `${infer Literal}` ? Literal : string;
Cette définition de type récursive décompose le littéral de modèle en ses parties constitutives et convertit chaque partie en un motif d'expression régulière.
Étape 2 : Implémenter la Fonction de Validation
Ensuite, nous implémentons la fonction de validation qui prend le type littéral de modèle et la chaîne à valider en entrée. Cette fonction utilise l'expression régulière générée par TemplateLiteralToRegex pour tester la chaîne.
function isValid(str: string, templateType: T): boolean {
const regexPattern = `^${convertTemplateLiteralToRegex(templateType)}$`;
const regex = new RegExp(regexPattern);
return regex.test(str);
}
function convertTemplateLiteralToRegex(templateType: T): string {
// Conversion de base pour les chaînes littérales - étendre ceci pour des scénarios plus complexes
return templateType.replace(/[.*+?^${}()|\[\]]/g, '\\$&'); // Échapper les caractères spéciaux des expressions régulières
}
Cette fonction échappe les caractères spéciaux des expressions régulières et crée une expression régulière à partir du type littéral de modèle, puis teste la chaîne par rapport à cette expression régulière.
Étape 3 : Utiliser le Moteur de Validation
Maintenant, vous pouvez utiliser la fonction isValid pour valider les chaînes par rapport à vos types littéraux de modèles au moment de l'exécution.
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
const userInput1 = "USD-100";
const userInput2 = "CAD-50";
console.log(`'${userInput1}' est valide: ${isValid(userInput1, "USD-100" )}`); // true
console.log(`'${userInput2}' est valide: ${isValid(userInput2, "USD-100")}`); // false
console.log(`'${userInput1}' est valide: ${isValid(userInput1, `USD-${100}`)}`); // true
console.log(`'${userInput2}' est valide: ${isValid(userInput2, `USD-${100}`)}`); // false
Cet exemple démontre comment utiliser la fonction isValid pour valider l'entrée utilisateur par rapport au type FormattedCurrencyString. Le résultat affichera si les chaînes d'entrée sont considérées comme valides ou non, en fonction du littéral de modèle spécifié.
Scénarios de Validation Avancés
Le moteur de validation de base peut être étendu pour gérer des scénarios plus complexes, tels que les unions, les types conditionnels et les types récursifs.
Gérer les Unions
Pour gérer les unions, vous pouvez modifier le type TemplateLiteralToRegex pour générer une expression régulière qui correspond à n'importe quel membre de l'union.
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
function isValidCurrencyCode(str: string, templateType: T): boolean {
const currencyCodes: CurrencyCode[] = ["USD", "EUR", "GBP"];
return currencyCodes.includes(str as CurrencyCode);
}
function isValidUnionFormattedCurrencyString(str: string): boolean {
const parts = str.split('-');
if(parts.length !== 2) return false;
const [currencyCode, amount] = parts;
if (!isValidCurrencyCode(currencyCode, currencyCode)) return false;
if (isNaN(Number(amount))) return false;
return true;
}
console.log(`'USD-100' is valid formatted string: ${isValidUnionFormattedCurrencyString('USD-100')}`);
console.log(`'CAD-50' is valid formatted string: ${isValidUnionFormattedCurrencyString('CAD-50')}`);
Gérer les Types Conditionnels
Les types conditionnels peuvent être gérés en évaluant la condition au moment de l'exécution et en générant différentes expressions régulières en fonction du résultat.
type IsString = T extends string ? true : false;
// Cet exemple nécessite une logique plus avancée et n'est pas entièrement implémentable à l'aide d'une simple expression régulière.
// Les protections de type d'exécution offrent une solution plus robuste dans ce scénario spécifique.
// Le code ci-dessous est illustratif et nécessiterait une adaptation pour gérer des types conditionnels complexes.
function isString(value: any): value is string {
return typeof value === 'string';
}
function isValidConditionalType(value: any): boolean {
return isString(value);
}
console.log(`'hello' is a string: ${isValidConditionalType('hello')}`);
console.log(`123 is a string: ${isValidConditionalType(123)}`);
Gérer les Types Récursifs
Les types récursifs peuvent être gérés en définissant une fonction récursive qui génère le motif d'expression régulière. Cependant, veillez à éviter la récursion infinie et les erreurs de dépassement de pile. Pour une récursion profonde, les approches itératives avec des limites appropriées sont cruciales.
Alternatives aux Expressions Régulières
Bien que les expressions régulières soient un outil puissant pour la validation de chaînes, elles peuvent être complexes et difficiles à maintenir. D'autres approches de validation d'exécution incluent :
- Fonctions de Validation Personnalisées : Écrivez des fonctions personnalisées pour valider des types spécifiques en fonction des exigences de votre application.
- Protections de Type : Utilisez des protections de type pour réduire le type d'une variable au moment de l'exécution.
- Bibliothèques de Validation : Tirez parti des bibliothèques de validation existantes comme Zod ou Yup pour simplifier le processus de validation.
Zod, par exemple, fournit une déclaration basée sur un schéma qui se traduit par une exécution de validation :
import { z } from 'zod';
const CurrencyCodeSchema = z.enum(['USD', 'EUR', 'GBP']);
const FormattedCurrencyStringSchema = z.string().regex(new RegExp(`^${CurrencyCodeSchema.enum.USD}|${CurrencyCodeSchema.enum.EUR}|${CurrencyCodeSchema.enum.GBP}-[0-9]+$`));
try {
const validCurrency = FormattedCurrencyStringSchema.parse("USD-100");
console.log("Valid Currency:", validCurrency);
} catch (error) {
console.error("Invalid Currency:", error);
}
try {
const invalidCurrency = FormattedCurrencyStringSchema.parse("CAD-50");
console.log("Valid Currency:", invalidCurrency); //This won't execute if parse fails.
} catch (error) {
console.error("Invalid Currency:", error);
}
Meilleures Pratiques pour la Validation d'Exécution
Lors de l'implémentation de la validation d'exécution, gardez à l'esprit les meilleures pratiques suivantes :
- Valider à la Frontière : Validez les données dès qu'elles entrent dans votre système (par exemple, entrée utilisateur, réponses API).
- Fournir des Messages d'Erreur Clairs : Générez des messages d'erreur informatifs pour aider les utilisateurs à comprendre pourquoi leur entrée n'est pas valide.
- Utiliser une Stratégie de Validation Cohérente : Adoptez une stratégie de validation cohérente dans votre application pour assurer l'intégrité des données.
- Tester Votre Logique de Validation : Testez minutieusement votre logique de validation pour vous assurer qu'elle identifie correctement les données valides et non valides.
- Équilibrer Performance et Sécurité : Optimisez votre logique de validation pour la performance tout en vous assurant qu'elle empêche efficacement les vulnérabilités de sécurité. Évitez les expressions régulières trop complexes qui conduisent à un déni de service.
Considérations d'Internationalisation
Lorsqu'il s'agit de la validation de chaînes dans un contexte mondial, vous devez tenir compte de l'internationalisation (i18n) et de la localisation (l10n). Différentes langues peuvent avoir des règles différentes pour formater les chaînes, telles que les dates, les nombres et les valeurs monétaires.
Par exemple, le symbole monétaire de l'euro (€) peut apparaître avant ou après le montant, selon la langue. De même, le séparateur décimal peut être un point (.) ou une virgule (,).
Pour gérer ces variations, vous pouvez utiliser des bibliothèques d'internationalisation comme Intl, qui fournit des API pour formater et analyser les données sensibles à la langue. Par exemple, vous pouvez adapter l'exemple précédent pour gérer différents formats de devise :
function isValidCurrencyString(currencyString: string, locale: string): boolean {
try {
const formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: currencyString.substring(0,3) }); //Very basic example
//Attempt to parse the currency using formatter. This example is intentionally very simple.
return true;
} catch (error) {
return false;
}
}
console.log(`USD-100 is valid for en-US: ${isValidCurrencyString('USD-100', 'en-US')}`);
console.log(`EUR-100 is valid for fr-FR: ${isValidCurrencyString('EUR-100', 'fr-FR')}`);
Cet extrait de code fournit un exemple fondamental. Une internationalisation appropriée nécessite une gestion plus approfondie, en utilisant potentiellement des bibliothèques ou des API externes spécifiquement conçues pour le formatage et la validation des devises dans différentes langues.
Conclusion
La validation d'exécution est un élément essentiel de la construction d'applications TypeScript robustes et fiables. En combinant les types littéraux de modèles de TypeScript avec des expressions régulières ou des méthodes de validation alternatives, vous pouvez créer un moteur puissant pour vérifier la validité des chaînes au moment de l'exécution.
Cette approche améliore la sécurité des types, prévient les erreurs inattendues et améliore la qualité globale de votre code. Au fur et à mesure que vous construisez des applications plus complexes, envisagez d'intégrer la validation d'exécution pour vous assurer que vos données sont conformes aux types et formats attendus.
Exploration Supplémentaire
- Explorez les techniques d'expressions régulières avancées pour des scénarios de validation plus complexes.
- Enquêtez sur les bibliothèques de validation comme Zod et Yup pour la validation basée sur un schéma.
- Envisagez d'utiliser des techniques de génération de code pour générer automatiquement des fonctions de validation à partir des types TypeScript.
- Étudiez les bibliothèques et les API d'internationalisation pour gérer les données sensibles à la langue.