Types Avancés et Décorateurs en TypeScript

Les Types Avancés en TypeScript

Les Génériques

TypeScript exige la déclaration des types lors de la définition de variables et de fonctions. Les génériques permettent de passer des types en tant que paramètres, rendant le code réutilisable pour diverses structures de données sans sacrifier la sécurité de type.

// Sans génériques, des fonctions séparées seraient nécessaires pour chaque type
function identityString(val: string): string { return val; }
function identityNumber(val: number): number { return val; }

// Avec les génériques, une seule fonction peut gérer plusieurs types
function echo<T>(input: T): T {
 return input;
}

// Les génériques peuvent accepter plusieurs paramètres de type
const createTypedArray = <T, U>(initialValue: T, count: number, _otherParam: U): T[] => {
 console.log(`Type de T: ${typeof initialValue}`); // Affiche le type d'exécution, pas le paramètre de type
 // _otherParam (U) est un exemple de paramètre de type non utilisé dans le corps mais utile pour la signature.
 return new Array(count).fill(initialValue);
};

// Utilisation
let myArray = createTypedArray("salut", 3, 123); // myArray est de type string[]
// console.log(createTypedArray(5, 2, true)); // Retourne [5, 5] et 'Type de T: number'

Contraintes de Génériques

Il est possible de limiter les types acceptables pour un paramètre générique en utilisant le mot-clé extends.

// Contrainte pour garantir la présence d'une propriété 'longueur'
const logLength = <T extends { length: number }>(data: T): number => {
 return data.length;
};
// Exemple: logLength("bonjour") fonctionne, logLength([1, 2, 3]) fonctionne, logLength({ length: 10 }) fonctionne.
// logLength(5) provoquerait une erreur de compilation.

// Contrainte sur des valeurs littérales spécifiques
const processStatusCode = <T extends "success" | "failure" | 200 | 400>(status: T): string => {
 return String(status);
};

// Contrainte sur des types fondamentaux
const formatInput = <T extends string | number>(value: T): string => {
 return String(value);
};

// Valeur par défaut pour un générique
const getOrDefault = <T extends string | number = string>(input?: T): string => {
 return String(input || "");
};
// getOrDefault() utilisera 'string' par défaut, getOrDefault(123) utilisera 'number'.

Le type utilitaire Partial

Partial<T> rend toutes les propriétés d'un type T facultatives.

interface Product {
 id: number;
 name: string;
 price: number;
 description: string;
}

type ProductUpdatePayload = Partial<Product>;
// Équivalent à:
// {
//   id?: number;
//   name?: string;
//   price?: number;
//   description?: string;
// }

Le type utilitaire Record

Record<K, V> construit un type d'objet où les clés sont de type K et les valeurs sont de type V. K peut être un type littéral ou une union de littéraux.

// Créer un type où les clés sont des chaînes et les valeurs sont des nombres
type InventoryCount = Record<string, number>;
const stock: InventoryCount = {
 apples: 150,
 oranges: 80,
 bananas: 200,
};

// Créer une correspondance pour des statuts d'application spécifiques
type AppStatusDisplay = Record<'running' | 'paused' | 'stopped', string>;
const statusLabels: AppStatusDisplay = {
 running: 'En cours',
 paused: 'En pause',
 stopped: 'Arrêté',
};

// Exemple dans une configuration d'application
type WidgetConfig = Record<string, { title: string; enabled: boolean }>;

Les types utilitaires Exclude et Extract

Exclude<T, U> construit un type en excluant des membres de T qui sont assignables à U. Extract<T, U> construit un type en extrayant de T les membres qui sont assignables à U.

type PaymentStatus = "pending" | "completed" | "failed" | "refunded" | "cancelled";

// Exclure "failed" et "cancelled" des statuts normaux
type ActivePaymentStatus = Exclude<PaymentStatus, "failed" | "cancelled">;
// Résultat: "pending" | "completed" | "refunded"

// Extraire les statuts terminaux
type FinalPaymentStatus = Extract<PaymentStatus, "completed" | "failed" | "refunded">;
// Résultat: "completed" | "failed" | "refunded"

Types Union et Types Intersection

Un type union (|) combine plusieurs types, indiquant qu'une valeur peut être de l'un ou l'autre des types listés. Un type intersection (&) combine plusieurs types, indiquant qu'une valeur doit avoir toutes les propriétés de tous les types listés.

type ConfigA = { theme: string };
type ConfigB = { debugMode: boolean };

type AppSettingsUnion = ConfigA | ConfigB; // Une valeur peut avoir soit 'theme', soit 'debugMode'
type AppSettingsIntersection = ConfigA & ConfigB; // Une valeur doit avoir 'theme' ET 'debugMode'

const settingsU1: AppSettingsUnion = { theme: 'dark' }; // Valide
const settingsU2: AppSettingsUnion = { debugMode: true }; // Valide
// const settingsU3: AppSettingsUnion = { theme: 'light', debugMode: false }; // Valide aussi, car c'est un sur-ensemble de l'un des types

const settingsI: AppSettingsIntersection = { theme: 'light', debugMode: false }; // Valide, doit avoir les deux
// const settingsI_invalid: AppSettingsIntersection = { theme: 'dark' }; // Erreur: 'debugMode' manque

Les Gardes de Type (Type Guards)

Les gardes de type sont des expressions qui effectuent une vérification de type à l'exécution et garantissent qu'un certain type est respecté dans un bloc conditionnel. Bien qu'utiles, leur fiabilité dépend de la logique implémentée.

console.log(typeof someVariable); // Affiche le type primitif de la variable
console.log(someObject instanceof MyClass); // Vérifie si un objet est une instance d'une classe

interface ProductItem {
 id: string;
 name: string;
 price?: number; // Propriété facultative
}

// Fonction de garde de type personnalisée
function isProductItem(item: any): item is ProductItem {
 return item && typeof item.id === "string" && typeof item.name === "string";
}

let unknownData: any = { id: "p1", name: "Laptop" };

if (isProductItem(unknownData)) {
 // Dans ce bloc, unknownData est garanti d'être de type ProductItem
 console.log(`Produit: ${unknownData.name}, ID: ${unknownData.id}`);
 if (unknownData.price) {
   console.log(`Prix: ${unknownData.price}`);
 }
}

Types Conditionnels

Les types conditionnels permettent de définir un type qui dépend d'une condition. La syntaxe est T extends U ? X : Y, signifiant "si le type T est assignable à U, alors le type résultant est X, sinon c'est Y".

type ExtractPromiseValue<T> = T extends Promise<infer U> ? U : T;
// Si T est un Promise, ce type retourne le type qu'il résout (inféré par 'infer U').
// Sinon, il retourne T lui-même.

type StringPromise = Promise<string>;
type NumberArrayPromise = Promise<number[]>;

type ResolvedString = ExtractPromiseValue<StringPromise>;      // string
type ResolvedNumberArray = ExtractPromiseValue<NumberArrayPromise>; // number[]
type RegularNumber = ExtractPromiseValue<number>;             // number
type ResolvedAny = ExtractPromiseValue<Promise<any>>;         // any

Fichiers de Déclaration (.d.ts)

Les fichiers de déclaration TypeScript (extension .d.ts) décrivant la forme des modules, fonctions, variables, etc., permettant à TypeScript de les utiliser en toute sécurité, même si leur implémentation est en JavaScript pur. Ils permettent de déclarer des entités globales ou de modules.

// dans un fichier par exemple 'global.d.ts'
declare const API_BASE_URL: string; // Déclare une constante globale
declare function logActivity(message: string, level?: 'info' | 'warn' | 'error'): void; // Déclare une fonction globale

// Pour qu'un fichier .d.ts ne soit pas considéré comme un "script global"
// et puisse tout de même ajouter des déclarations globales, on peut utiliser:
export {};
declare global {
 const CURRENT_APP_VERSION: string; // Autre constante globale
 function generateUniqueId(): string; // Autre fonction globale
}

L'utilisation de export {}; ou declare global {} dans un fichier .d.ts garantit qu'il est traité comme un module, mais permet tout de même d'ajouter des déclarations à l'espace de noms global de TypeScript.

Les Décorateurs TypeScript

Les décorateurs sont des fonctions spéciales qui peuvent être attachées à des classes, des méthodes, des accesseurs, des propriétés ou des paramètres à l'aide de la syntaxe @. Ils permettent da'jouter des annotations et une méta-programmation au code. C'est une fonctionnalité expérimentale en TypeScript, mais largement utilisée dans des frameworks comme Angular et Vue (via des bibliothèques).

Il est recommandé de définir les décorateurs avec des fonctions régulières plutôt que des fonctions fléchées.

Décorateur de Classe

Un décorateur de classe est appliqué directement avant la définition d'une classe. Il reçoit le constructeur de la classe comme argument.

function auditCreation(targetClass: Function): void {
 console.log(`[Décorateur] La classe "${targetClass.name}" a été définie.`);
}

@auditCreation
class ConfigurationService {
 constructor() {
   console.log('Instance de ConfigurationService créée.');
 }
}
// Ordre d'exécution:
// [Décorateur] La classe "ConfigurationService" a été définie. (Exécuté au moment de la définition de la classe)
const config = new ConfigurationService();
// Instance de ConfigurationService créée. (Exécuté à l'instanciation de la classe)

Fabriques de Décorateurs (Decorator Factories)

Pour passer des arguments à un décorateur, on utilise une fabrique de décorateurs. C'est une fonction qui retourne la fonction du décorateur réel.

// Fabrique de décorateur pour ajouter un attribut de version à une classe
function AddVersion(versionString: string) {
 // Le décorateur réel
 return function (constructor: Function) {
   // Ajoute une propriété statique 'version' à la classe
   Object.defineProperty(constructor.prototype, 'appVersion', {
     value: versionString,
     writable: false,
     configurable: false,
   });
   console.log(`Classe décorée avec la version: ${versionString}`);
 };
}

// Utilisation de la fabrique de décorateur avec un argument
@AddVersion('2.1.0-beta')
class ApplicationCore {
 appName: string;
 constructor(name: string) {
   this.appName = name;
 }
}

// Test
const appInstance = new ApplicationCore("MonApp");
console.log((appInstance as any).appVersion); // Affiche: 2.1.0-beta

Décorateur de Méthode

Les décorateurs de méthode reçoivent trois arguments: la cible (prototype de la classe pour les membres d'instance), le nom de la propriété/méthode, et le descripteur de propriété de la méthode.

Le descripteur de propriété (PropertyDescriptor) contient des informations sur la méthode originale :

descriptor = {
 value: function() {}, // La fonction de la méthode originale
 writable: true,      // Peut-on modifier la valeur de la propriété ?
 enumerable: true,    // La propriété est-elle énumérable lors de l'itération des clés ?
 configurable: true   // Peut-on modifier le descripteur de la propriété ou supprimer la propriété ?
}
// Décorateur de méthode: intercepte l'appel de méthode pour ajouter des logs
function logMethodCall(target: any, propertyName: string, descriptor: PropertyDescriptor) {
 const originalMethod = descriptor.value; // Sauvegarder la méthode originale

 // Remplacer la méthode originale par une nouvelle fonction
 descriptor.value = function (...args: any[]) {
   console.log(`[Début] Appel de la méthode "${propertyName}" avec arguments: ${JSON.stringify(args)}`);
   const result = originalMethod.apply(this, args); // Appeler la méthode originale avec le bon contexte 'this'
   console.log(`[Fin] La méthode "${propertyName}" a retourné: ${JSON.stringify(result)}`);
   return result; // Retourner le résultat de la méthode originale
 };
}

class MathOperations {
 @logMethodCall
 add(num1: number, num2: number): number {
   return num1 + num2;
 }

 @logMethodCall
 subtract(num1: number, num2: number): number {
   return num1 - num2;
 }
}

const ops = new MathOperations();
ops.add(10, 5);
// [Début] Appel de la méthode "add" avec arguments: [10,5]
// [Fin] La méthode "add" a retourné: 15
ops.subtract(20, 7);
// [Début] Appel de la méthode "subtract" avec arguments: [20,7]
// [Fin] La méthode "subtract" a retourné: 13

Décorateurs Vue.js (via vue-property-decorator)

Les décorateurs sont particulièrement utiles dans le contexte de frameworks comme Vue.js, notamment avec des bibliothèques comme vue-property-decorator qui les intègrent pour une syntaxe plus élégante.

@Component

Ce décorateur enregistre une classe TypeScript comme un composant Vue.

// Extrait de 'vue-class-component'
import { Vue, Component } from 'vue-class-component';

@Component({
 name: 'ProductCatalog',
 components: { /* SubComponent */ },
 // ... autres options de composant Vue
})
export default class ProductCatalog extends Vue {
 products: string[] = ['Pommes', 'Poires'];

 created() {
   console.log('Composant ProductCatalog créé.');
 }
}

@Watch

Permet de surveiller les changements d'une propriété réactive du composant et de déclencher une méthode en conséquence.

<template>
 <div>
   <div>Quantité réactive : {{ quantity }}</div>
   <button @click="incrementQuantity">Augmenter la quantité</button>
 </div>
</template>

<script lang="ts">
import { Vue, Component, Watch } from 'vue-property-decorator';

@Component
export default class QuantityMonitor extends Vue {
 quantity = 0; // Une propriété de données réactive

 // Surveille les changements de la propriété 'quantity'
 @Watch('quantity')
 onQuantityChanged(newValue: number, oldValue: number) {
   console.log(`La quantité est passée de ${oldValue} à ${newValue}`);
 }

 incrementQuantity() {
   this.quantity++;
 }
}
</script>

Décorateur de Propriété

Un décorateur de propriété est appliqué à la définition d'une propriété de classe. Il reçoit deux arguments : la cible et le nom de la propriété. Il est souvent utilisé pour des métadonnées ou des transformations de propriétés.

@Prop

Utilisé dans Vue.js pour déclarer des propriétés (props) passées par un copmosant parent.

@Emit

Utilisé dans Vue.js pour émettre des événements vers un composant parent.

Exemple Pratique avec Vue.js : Communication Parent-Enfant

Composant Parent (ParentComponent.vue)

<template>
 <div>
   <h3>Composant Parent</h3>
   <p>Compteur 1 du Parent: {{ parentCounter1 }}</p>
   <p>Compteur 2 du Parent: {{ parentCounter2 }}</p>

   <ChildComponent
     :valueA="parentCounter1"
     :valueB="parentCounter2"
     @incrementA="handleIncrementA"
     @incrementB="handleIncrementB"
   />
 </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator';
import ChildComponent from './ChildComponent.vue';

@Component({
 components: { ChildComponent }
})
export default class ParentComponent extends Vue {
 parentCounter1 = 0;
 parentCounter2 = 0;

 handleIncrementA(message: string) {
   this.parentCounter1++;
   console.log('Événement A reçu:', message);
 }

 handleIncrementB(message: string) {
   this.parentCounter2++;
   console.log('Événement B reçu:', message);
 }
}
</script>

Composant Enfant (ChildComponent.vue)

<template>
 <div style="border: 1px dashed gray; padding: 10px; margin-top: 10px;">
   <h4>Composant Enfant</h4>
   <p>Valeur A (prop): {{ valueA }}</p>
   <button @click="emitIncrementA">Incrementer A (Parent)</button>

   <p v-if="displayValueB">Valeur B (prop): {{ valueB }}</p>
   <button v-if="displayValueB" @click="emitIncrementB">Incrementer B (Parent)</button>
 </div>
</template>

<script lang="ts">
import { Vue, Component, Prop, Emit } from 'vue-property-decorator';

@Component
export default class ChildComponent extends Vue {
 // ======================
 // Parent -> Enfant: Décorateur @Prop
 // ======================
 @Prop(Number) readonly valueA!: number; // readonly pour indiquer que l'enfant ne doit pas modifier la prop
 @Prop(Number) readonly valueB!: number;

 displayValueB = true; // Une donnée interne au composant

 // ======================
 // Enfant -> Parent: Décorateur @Emit
 // ======================
 @Emit('incrementA') // Émet un événement nommé 'incrementA'
 emitIncrementA() {
   console.log('Émission de incrementA');
   return 'Message de l\'enfant pour A'; // Ces données sont passées au gestionnaire de l'événement
 }

 @Emit('incrementB') // Émet un événement nommé 'incrementB'
 emitIncrementB() {
   console.log('Émission de incrementB');
   return 'Message de l\'enfant pour B';
 }
}
</script>

Étiquettes: TypeScript génériques décorateurs Utility Types Type Guards

Publié le 4 juin à 03h58