Découvrez les décorateurs JavaScript : ajoutez des métadonnées, transformez les classes/méthodes et améliorez votre code de manière propre et déclarative.
Décorateurs JavaScript : Métadonnées et Transformation
Les décorateurs JavaScript, une fonctionnalité inspirée de langages comme Python et Java, offrent un moyen puissant et expressif d'ajouter des métadonnées et de transformer des classes, des méthodes, des propriétés et des paramètres. Ils proposent une syntaxe propre et déclarative pour améliorer la fonctionnalité du code et promouvoir la séparation des préoccupations. Bien qu'ils soient encore un ajout relativement nouveau à l'écosystème JavaScript, les décorateurs gagnent en popularité, en particulier au sein de frameworks comme Angular et de bibliothèques qui exploitent les métadonnées pour l'injection de dépendances et d'autres fonctionnalités avancées. Cet article explore les principes fondamentaux des décorateurs JavaScript, leur application et leur potentiel pour créer des bases de code plus maintenables et extensibles.
Que sont les décorateurs JavaScript ?
À la base, les décorateurs sont des types de déclarations spéciales qui peuvent être attachées à des classes, des méthodes, des accesseurs, des propriétés ou des paramètres. Ils utilisent la syntaxe @expression
, où expression
doit être évaluée en une fonction qui sera appelée à l'exécution avec des informations sur la déclaration décorée. Les décorateurs agissent essentiellement comme des fonctions qui modifient ou étendent le comportement de l'élément décoré.
Pensez aux décorateurs comme un moyen d'envelopper ou d'augmenter du code existant sans le modifier directement. Ce principe, connu sous le nom de patron de conception Décorateur en génie logiciel, vous permet d'ajouter dynamiquement des fonctionnalités à un objet.
Activer les décorateurs
Bien que les décorateurs fassent partie de la norme ECMAScript, ils ne sont pas activés par défaut dans la plupart des environnements JavaScript. Pour les utiliser, vous devrez généralement configurer vos outils de build. Voici comment activer les décorateurs dans certains environnements courants :
- TypeScript : Les décorateurs sont pris en charge nativement dans TypeScript. Assurez-vous que l'option du compilateur
experimentalDecorators
est définie surtrue
dans votre fichiertsconfig.json
:
{
"compilerOptions": {
"target": "esnext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true, // Optionnel, mais souvent utile
"module": "commonjs", // Ou un autre système de modules comme "es6" ou "esnext"
"moduleResolution": "node"
}
}
- Babel : Si vous utilisez Babel, vous devrez installer et configurer le plugin
@babel/plugin-proposal-decorators
:
npm install --save-dev @babel/plugin-proposal-decorators
Ensuite, ajoutez le plugin à votre configuration Babel (par exemple, .babelrc
ou babel.config.js
) :
{
"plugins": [["@babel/plugin-proposal-decorators", { "version": "2023-05" }]]
}
L'option version
est importante et doit correspondre à la version de la proposition de décorateurs que vous ciblez. Consultez la documentation du plugin Babel pour la dernière version recommandée.
Types de décorateurs
Il existe plusieurs types de décorateurs, chacun conçu pour des éléments spécifiques :
- Décorateurs de classe : Appliqués aux classes.
- Décorateurs de méthode : Appliqués aux méthodes au sein d'une classe.
- Décorateurs d'accesseur : Appliqués aux accesseurs getter ou setter.
- Décorateurs de propriété : Appliqués aux propriétés d'une classe.
- Décorateurs de paramètre : Appliqués aux paramètres d'une méthode ou d'un constructeur.
Décorateurs de classe
Les décorateurs de classe sont appliqués au constructeur d'une classe et peuvent être utilisés pour observer, modifier ou remplacer une définition de classe. Ils reçoivent le constructeur de la classe comme unique argument.
Exemple :
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
// Toute tentative d'ajout de propriétés à la classe scellée ou à son prototype échouera
Dans cet exemple, le décorateur @sealed
empêche toute modification ultérieure de la classe Greeter
et de son prototype. Cela peut être utile pour garantir l'immuabilité ou prévenir les modifications accidentelles.
Décorateurs de méthode
Les décorateurs de méthode sont appliqués aux méthodes au sein d'une classe. Ils reçoivent trois arguments :
target
: Le prototype de la classe (pour les méthodes d'instance) ou le constructeur de la classe (pour les méthodes statiques).propertyKey
: Le nom de la méthode décorée.descriptor
: Le descripteur de propriété pour la méthode.
Exemple :
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Appel de ${propertyKey} avec les arguments : ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`La méthode ${propertyKey} a retourné : ${result}`);
return result;
};
return descriptor;
}
class Calculator {
@log
add(x: number, y: number) {
return x + y;
}
}
const calculator = new Calculator();
calculator.add(2, 3); // Sortie : Appel de add avec les arguments : [2,3]
// La méthode add a retourné : 5
Le décorateur @log
journalise les arguments et la valeur de retour de la méthode add
. C'est un exemple simple de la manière dont les décorateurs de méthode peuvent être utilisés pour la journalisation, le profilage ou d'autres préoccupations transversales.
Décorateurs d'accesseur
Les décorateurs d'accesseur sont similaires aux décorateurs de méthode mais sont appliqués aux accesseurs getter ou setter. Ils reçoivent également les trois mêmes arguments : target
, propertyKey
et descriptor
.
Exemple :
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
set x(value: number) {
this._x = value;
}
}
const point = new Point(1, 2);
// Object.defineProperty(point, 'x', { configurable: true }); // Lèverait une erreur car 'x' n'est pas configurable
Le décorateur @configurable(false)
empêche le getter x
d'être reconfiguré, le rendant non configurable.
Décorateurs de propriété
Les décorateurs de propriété sont appliqués aux propriétés d'une classe. Ils reçoivent deux arguments :
target
: Le prototype de la classe (pour les propriétés d'instance) ou le constructeur de la classe (pour les propriétés statiques).propertyKey
: Le nom de la propriété décorée.
Exemple :
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false,
});
}
class Person {
@readonly
name: string;
constructor(name: string) {
this.name = name;
}
}
const person = new Person("Alice");
// person.name = "Bob"; // Cela provoquera une erreur en mode strict car 'name' est en lecture seule
Le décorateur @readonly
rend la propriété name
en lecture seule, empêchant sa modification après l'initialisation.
Décorateurs de paramètre
Les décorateurs de paramètre sont appliqués aux paramètres d'une méthode ou d'un constructeur. Ils reçoivent trois arguments :
target
: Le prototype de la classe (pour les méthodes d'instance) ou le constructeur de la classe (pour les méthodes statiques ou les constructeurs).propertyKey
: Le nom de la méthode ou du constructeur.parameterIndex
: L'index du paramètre dans la liste des paramètres.
Les décorateurs de paramètre sont souvent utilisés avec la réflexion pour stocker des métadonnées sur les paramètres d'une fonction. Ces métadonnées peuvent ensuite être utilisées à l'exécution pour l'injection de dépendances ou à d'autres fins. Pour que cela fonctionne correctement, vous devez activer l'option du compilateur emitDecoratorMetadata
dans votre fichier tsconfig.json
.
Exemple (avec reflect-metadata
) :
import 'reflect-metadata';
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata("required", target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata("required", existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor) {
let method = descriptor.value!;
descriptor.value = function (...args: any[]) {
let requiredParameters: number[] = Reflect.getOwnMetadata("required", target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (args[parameterIndex] === null || args[parameterIndex] === undefined) {
throw new Error(`Argument requis manquant à l'index ${parameterIndex}`);
}
}
}
return method.apply(this, args);
};
}
class User {
name: string;
age: number;
constructor(@required name: string, public surname: string, @required age: number) {
this.name = name;
this.age = age;
}
@validate
greet(prefix: string, @required salutation: string): string {
return `${prefix} ${salutation} ${this.name}`;
}
}
// Utilisation
try {
const user1 = new User("John", "Doe", 30);
console.log(user1.greet("Mr.", "Hello"));
const user2 = new User(undefined as any, "Doe", null as any);
} catch (error) {
console.error(error.message);
}
try {
const user = new User("John", "Doe", 30);
console.log(user.greet("Mr.", undefined as any));
} catch (error) {
console.error(error.message);
}
Dans cet exemple, le décorateur @required
marque les paramètres comme requis. Le décorateur @validate
utilise ensuite la réflexion (via reflect-metadata
) pour vérifier si les paramètres requis sont présents avant d'appeler la méthode. Cet exemple montre l'utilisation de base, et il est recommandé de créer une validation de paramètre robuste dans un scénario de production.
Pour installer reflect-metadata
:
npm install reflect-metadata --save
Utiliser les décorateurs pour les métadonnées
L'une des principales utilisations des décorateurs est d'attacher des métadonnées aux classes et à leurs membres. Ces métadonnées peuvent être utilisées à l'exécution à diverses fins, telles que l'injection de dépendances, la sérialisation et la validation. La bibliothèque reflect-metadata
fournit un moyen standard de stocker et de récupérer des métadonnées.
Exemple :
import 'reflect-metadata';
const TYPE_KEY = "design:type";
const PARAMTYPES_KEY = "design:paramtypes";
const RETURNTYPE_KEY = "design:returntype";
function Type(type: any) {
return Reflect.metadata(TYPE_KEY, type);
}
function LogType(target: any, propertyKey: string) {
const t = Reflect.getMetadata(TYPE_KEY, target, propertyKey);
console.log(`${target.constructor.name}.${propertyKey} type: ${t.name}`);
}
class Demo {
@LogType
public name: string;
constructor(name: string){
this.name = name;
}
}
Fabriques de décorateurs
Les fabriques de décorateurs sont des fonctions qui retournent un décorateur. Elles vous permettent de passer des arguments au décorateur, le rendant plus flexible et réutilisable.
Exemple :
function deprecated(deprecationReason: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.warn(`La méthode ${propertyKey} est obsolète : ${deprecationReason}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class LegacyComponent {
@deprecated("Utilisez newMethod à la place.")
oldMethod() {
console.log("Ancienne méthode appelée");
}
newMethod() {
console.log("Nouvelle méthode appelée");
}
}
const component = new LegacyComponent();
component.oldMethod(); // Sortie : La méthode oldMethod est obsolète : Utilisez newMethod à la place.
// Ancienne méthode appelée
La fabrique de décorateurs @deprecated
prend un message d'obsolescence en argument et journalise un avertissement lorsque la méthode décorée est appelée. Cela vous permet de marquer des méthodes comme obsolètes et de fournir des conseils aux développeurs sur la manière de migrer vers des alternatives plus récentes.
Cas d'utilisation concrets
Les décorateurs ont un large éventail d'applications dans le développement JavaScript moderne :
- Injection de dépendances : Les frameworks comme Angular s'appuient fortement sur les décorateurs pour l'injection de dépendances.
- Routage : Dans les applications web, les décorateurs peuvent être utilisés pour définir des routes pour les contrôleurs et les méthodes.
- Validation : Les décorateurs peuvent être utilisés pour valider les données d'entrée et s'assurer qu'elles respectent certains critères.
- Autorisation : Les décorateurs peuvent être utilisés pour appliquer des politiques de sécurité et restreindre l'accès à certaines méthodes ou ressources.
- Journalisation et profilage : Comme le montrent les exemples ci-dessus, les décorateurs peuvent être utilisés pour la journalisation et le profilage de l'exécution du code.
- Gestion de l'état : Les décorateurs peuvent s'intégrer avec des bibliothèques de gestion d'état pour mettre à jour automatiquement les composants lorsque l'état change.
Avantages de l'utilisation des décorateurs
- Amélioration de la lisibilité du code : Les décorateurs fournissent une syntaxe déclarative pour ajouter des fonctionnalités, rendant le code plus facile à comprendre et à maintenir.
- Séparation des préoccupations : Les décorateurs vous permettent de séparer les préoccupations transversales (par exemple, la journalisation, la validation, l'autorisation) de la logique métier principale.
- Réutilisabilité : Les décorateurs peuvent être réutilisés sur plusieurs classes et méthodes, réduisant ainsi la duplication de code.
- Extensibilité : Les décorateurs facilitent l'extension des fonctionnalités du code existant sans le modifier directement.
Défis et considérations
- Courbe d'apprentissage : Les décorateurs sont une fonctionnalité relativement nouvelle, et il peut falloir un certain temps pour apprendre à les utiliser efficacement.
- Compatibilité : Assurez-vous que votre environnement cible prend en charge les décorateurs et que vous avez correctement configuré vos outils de build.
- Débogage : Le débogage du code qui utilise des décorateurs peut être plus difficile que le débogage de code ordinaire, surtout si les décorateurs sont complexes.
- Surutilisation : Évitez de surutiliser les décorateurs, car cela peut rendre votre code plus difficile à comprendre et à maintenir. Utilisez-les de manière stratégique à des fins spécifiques.
- Surcharge à l'exécution : Les décorateurs peuvent introduire une certaine surcharge à l'exécution, surtout s'ils effectuent des opérations complexes. Tenez compte des implications sur les performances lors de l'utilisation de décorateurs dans des applications critiques en termes de performance.
Conclusion
Les décorateurs JavaScript sont un outil puissant pour améliorer la fonctionnalité du code et promouvoir la séparation des préoccupations. En fournissant une syntaxe propre et déclarative pour ajouter des métadonnées et transformer des classes, des méthodes, des propriétés et des paramètres, les décorateurs peuvent vous aider à créer des bases de code plus maintenables, réutilisables et extensibles. Bien qu'ils s'accompagnent d'une courbe d'apprentissage et de certains défis potentiels, les avantages de l'utilisation des décorateurs dans le bon contexte peuvent être significatifs. À mesure que l'écosystème JavaScript continue d'évoluer, les décorateurs deviendront probablement une partie de plus en plus importante du développement JavaScript moderne.
Envisagez d'explorer comment les décorateurs peuvent simplifier votre code existant ou vous permettre d'écrire des applications plus expressives et maintenables. Avec une planification minutieuse et une solide compréhension de leurs capacités, vous pouvez tirer parti des décorateurs pour créer des solutions JavaScript plus robustes et évolutives.