Explorez les fonctionnalités avancées de TypeScript comme les types littéraux de gabarits et conditionnels pour un code plus expressif et maintenable.
Types avancés TypeScript : Maîtriser les types littéraux de gabarits et conditionnels
La force de TypeScript réside dans son puissant système de types. Bien que les types de base comme string, number et boolean soient suffisants pour de nombreux scénarios, des fonctionnalités avancées comme les types littéraux de gabarits et les types conditionnels débloquent un nouveau niveau d'expressivité et de sécurité de types. Ce guide offre un aperçu complet de ces types avancés, explorant leurs capacités et démontrant des applications pratiques.
Comprendre les types littéraux de gabarits
Les types littéraux de gabarits s'appuient sur les littéraux de gabarits de JavaScript, vous permettant de définir des types basés sur l'interpolation de chaînes de caractères. Cela permet la création de types qui représentent des motifs de chaînes spécifiques, rendant votre code plus robuste et prévisible.
Syntaxe et utilisation de base
Les types littéraux de gabarits utilisent des apostrophes inversées (`) pour encadrer la définition du type, similaire aux littéraux de gabarits JavaScript. À l'intérieur des apostrophes inversées, vous pouvez interpoler d'autres types en utilisant la syntaxe ${}. C'est là que la magie opère – vous créez essentiellement un type qui est une chaîne de caractères, construite au moment de la compilation en fonction des types à l'intérieur de l'interpolation.
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/${string}`;
// Exemple d'utilisation
const getEndpoint: APIEndpoint = "/api/users"; // Valide
const postEndpoint: APIEndpoint = "/api/products/123"; // Valide
const invalidEndpoint: APIEndpoint = "/admin/settings"; // TypeScript n'affichera pas d'erreur ici car `string` peut ĂŞtre n'importe quoi
Dans cet exemple, APIEndpoint est un type qui représente toute chaîne commençant par /api/. Bien que cet exemple de base soit utile, le véritable pouvoir des types littéraux de gabarits émerge lorsqu'ils sont combinés avec des contraintes de types plus spécifiques.
Combinaison avec les types union
Les types littéraux de gabarits brillent vraiment lorsqu'ils sont utilisés avec des types union. Cela vous permet de créer des types qui représentent un ensemble spécifique de combinaisons de chaînes.
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIPath = "users" | "products" | "orders";
type APIEndpoint = `/${APIPath}/${HTTPMethod}`;
// Points de terminaison API valides
const getUsers: APIEndpoint = "/users/GET";
const postProducts: APIEndpoint = "/products/POST";
// Points de terminaison API invalides (entraîneront des erreurs TypeScript)
// const invalidEndpoint: APIEndpoint = "/users/PATCH"; // Erreur : "/users/PATCH" n'est pas assignable au type "/users/GET" | "/users/POST" | "/users/PUT" | "/users/DELETE" | "/products/GET" | "/products/POST" | ... 3 de plus ... | "/orders/DELETE".
Maintenant, APIEndpoint est un type plus restrictif qui n'autorise que des combinaisons spécifiques de chemins d'API et de méthodes HTTP. TypeScript signalera toute tentative d'utilisation de combinaisons invalides, améliorant ainsi la sécurité des types.
Manipulation de chaînes avec les types littéraux de gabarits
TypeScript fournit des types intrinsèques de manipulation de chaînes qui fonctionnent de manière transparente avec les types littéraux de gabarits. Ces types vous permettent de transformer des chaînes au moment de la compilation.
- Uppercase : Convertit une chaîne en majuscules.
- Lowercase : Convertit une chaîne en minuscules.
- Capitalize : Met en majuscule la première lettre d'une chaîne.
- Uncapitalize : Met en minuscule la première lettre d'une chaîne.
type Greeting = "hello world";
type UppercaseGreeting = Uppercase; // "HELLO WORLD"
type LowercaseGreeting = Lowercase; // "hello world"
type CapitalizedGreeting = Capitalize; // "Hello world"
type UncapitalizedGreeting = Uncapitalize; // "hello world"
Ces types de manipulation de chaînes sont particulièrement utiles pour générer automatiquement des types basés sur des conventions de nommage. Par exemple, vous pourriez dériver des types d'action à partir de noms d'événements ou vice versa.
Applications pratiques des types littéraux de gabarits
- Définition des points de terminaison d'API : Comme démontré ci-dessus, définir des points de terminaison d'API avec des contraintes de types précises.
- Gestion des événements : Créer des types pour les noms d'événements avec des préfixes et suffixes spécifiques.
- Génération de classes CSS : Générer des noms de classes CSS basés sur les noms et les états des composants.
- Construction de requêtes de base de données : Assurer la sécurité des types lors de la construction de requêtes de base de données.
Exemple international : Formatage de devises
Imaginez que vous développez une application financière qui prend en charge plusieurs devises. Vous pouvez utiliser les types littéraux de gabarits pour faire respecter le formatage correct des devises.
type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";
type CurrencyFormat = `${number} ${T}`;
const priceUSD: CurrencyFormat<"USD"> = "100 USD"; // Valide
const priceEUR: CurrencyFormat<"EUR"> = "50 EUR"; // Valide
// const priceInvalid: CurrencyFormat<"USD"> = "100 EUR"; // Erreur : Le type 'string' n'est pas assignable au type '`${number} USD`'.
function formatCurrency(amount: number, currency: T): CurrencyFormat {
return `${amount} ${currency}`;
}
const formattedUSD = formatCurrency(250, "USD"); // Type : "250 USD"
const formattedEUR = formatCurrency(100, "EUR"); // Type : "100 EUR"
Cet exemple garantit que les valeurs monétaires sont toujours formatées avec le code de devise correct, évitant ainsi les erreurs potentielles.
Plongée dans les types conditionnels
Les types conditionnels introduisent une logique de branchement dans le système de types de TypeScript, vous permettant de définir des types qui dépendent d'autres types. Cette fonctionnalité est incroyablement puissante pour créer des définitions de types hautement flexibles et réutilisables.
Syntaxe et utilisation de base
Les types conditionnels utilisent le mot-clé infer et l'opérateur ternaire (condition ? typeVrai : typeFaux) pour définir des conditions de type.
type IsString = T extends string ? true : false;
type StringCheck = IsString; // type StringCheck = true
type NumberCheck = IsString; // type NumberCheck = false
Dans cet exemple, IsString est un type conditionnel qui vérifie si T est assignable à string. Si c'est le cas, le type se résout en true ; sinon, il se résout en false.
Le mot-clé infer
Le mot-clé infer vous permet d'extraire un type d'un type. Ceci est particulièrement utile lorsque vous travaillez avec des types complexes comme les types de fonctions ou les types de tableaux.
type ReturnType any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType; // type AddReturnType = number
Dans cet exemple, ReturnType extrait le type de retour d'un type de fonction T. La partie infer R du type conditionnel déduit le type de retour et l'assigne à la variable de type R. Si T n'est pas un type de fonction, le type se résout en any.
Types conditionnels distributifs
Les types conditionnels deviennent distributifs lorsque le type vérifié est un paramètre de type nu. Cela signifie que le type conditionnel est appliqué à chaque membre du type union séparément.
type ToArray = T extends any ? T[] : never;
type NumberOrStringArray = ToArray; // type NumberOrStringArray = string[] | number[]
Dans cet exemple, ToArray convertit un type T en un type tableau. Étant donné que T est un paramètre de type nu (pas encapsulé dans un autre type), le type conditionnel est appliqué séparément à number et string, résultant en une union de number[] et string[].
Applications pratiques des types conditionnels
- Extraction des types de retour : Comme démontré ci-dessus, l'extraction du type de retour d'une fonction.
- Filtrage des types d'une union : Créer un type qui ne contient que des types spécifiques d'une union.
- Définition des types de fonctions surchargées : Créer différents types de fonctions en fonction des types d'entrée.
- Création de garde-types : Définir des fonctions qui réduisent le type d'une variable.
Exemple international : Gestion des différents formats de date
Différentes régions du monde utilisent différents formats de date. Vous pouvez utiliser des types conditionnels pour gérer ces variations.
type DateFormat = "YYYY-MM-DD" | "MM/DD/YYYY" | "DD.MM.YYYY";
type ParseDate = T extends "YYYY-MM-DD"
? { year: number; month: number; day: number; format: "YYYY-MM-DD" }
: T extends "MM/DD/YYYY"
? { month: number; day: number; year: number; format: "MM/DD/YYYY" }
: T extends "DD.MM.YYYY"
? { day: number; month: number; year: number; format: "DD.MM.YYYY" }
: never;
function parseDate(dateString: string, format: T): ParseDate {
// (L'implémentation gérerait les différents formats de date)
if (format === "YYYY-MM-DD") {
const [year, month, day] = dateString.split("-").map(Number);
return { year, month, day, format } as ParseDate;
} else if (format === "MM/DD/YYYY") {
const [month, day, year] = dateString.split("/").map(Number);
return { month, day, year, format } as ParseDate;
} else if (format === "DD.MM.YYYY") {
const [day, month, year] = dateString.split(".").map(Number);
return { day, month, year, format } as ParseDate;
} else {
throw new Error("Format de date invalide");
}
}
const parsedDateISO = parseDate("2023-10-27", "YYYY-MM-DD"); // Type : { year: number; month: number; day: number; format: "YYYY-MM-DD"; }
const parsedDateUS = parseDate("10/27/2023", "MM/DD/YYYY"); // Type : { month: number; day: number; year: number; format: "MM/DD/YYYY"; }
const parsedDateEU = parseDate("27.10.2023", "DD.MM.YYYY"); // Type : { day: number; month: number; year: number; format: "DD.MM.YYYY"; }
console.log(parsedDateISO.year); // Accède Ă l'annĂ©e en sachant qu'elle sera lĂ
Cet exemple utilise des types conditionnels pour définir différentes fonctions d'analyse de dates en fonction du format de date spécifié. Le type ParseDate garantit que l'objet retourné a les propriétés correctes en fonction du format.
Combinaison des types littéraux de gabarits et conditionnels
Le véritable pouvoir réside dans la combinaison des types littéraux de gabarits et des types conditionnels. Cela permet des manipulations de types incroyablement puissantes.
type EventName = `on${Capitalize}`;
type ExtractEventPayload = T extends EventName
? { type: T; payload: any } // Simplifié pour la démonstration
: never;
type ClickEvent = EventName<"click">; // "onClick"
type MouseOverEvent = EventName<"mouseOver">; // "onMouseOver"
// Exemple de fonction qui prend un type
function processEvent(event: T): ExtractEventPayload {
// Dans une implémentation réelle, nous déclencherions l'événement.
console.log(`Traitement de l'événement ${event}`);
// Dans une implémentation réelle, la charge utile serait basée sur le type d'événement.
return { type: event, payload: {} } as ExtractEventPayload;
}
// Notez que les types de retour sont très spécifiques :
const clickEvent = processEvent("onClick"); // { type: "onClick"; payload: any; }
const mouseOverEvent = processEvent("onMouseOver"); // { type: "onMouseOver"; payload: any; }
// Si vous utilisez d'autres chaînes, vous obtenez `never` :
// const someOtherEvent = processEvent("someOtherEvent"); // Le type est `never`
Bonnes pratiques et considérations
- Garder la simplicité : Bien que puissants, ces types avancés peuvent rapidement devenir complexes. Visez la clarté et la maintenabilité.
- Tester rigoureusement : Assurez-vous que vos définitions de types se comportent comme prévu en écrivant des tests unitaires complets.
- Documenter votre code : Documentez clairement le but et le comportement de vos types avancés pour améliorer la lisibilité du code.
- Considérer les performances : L'utilisation excessive de types avancés peut affecter le temps de compilation. Profilez votre code et optimisez si nécessaire.
Conclusion
Les types littéraux de gabarits et les types conditionnels sont des outils puissants dans l'arsenal de TypeScript. En maîtrisant ces types avancés, vous pouvez écrire un code plus expressif, maintenable et sûr en termes de types. Ces fonctionnalités vous permettent de capturer des relations complexes entre les types, d'appliquer des contraintes plus strictes et de créer des définitions de types hautement réutilisables. Adoptez ces techniques pour élever vos compétences en TypeScript et construire des applications robustes et évolutives pour un public mondial.