Ca y est TypeScript 5 est sorti. Et avec cette version l’arrivée du support des “vrais” decorators ! (pour anticiper la possible validation par la TC39 en 2024).
Voyons ça de suite avec la création de deux decorateurs : un premier pour se chauffer : un logger de méthode, puis nous passerons à la construction d’un décorateur de validation.
Logger de méthode
Imaginons une classe comme suit :
import { logged } from './decorators'; export class Wookiee { @logged('Wookiee') hurler(): void { console.log('Rowaaaaa'); } name: string|undefined = ''; }
Nous voulons ici exécuter notre methode et logger cet appel, pour l’exemple, dans la console.
Créons un decorator (bien typé) :
export function logged<This, Args extends any[], Return>(message = "Log"){
return function actualDecorator(originalMethod: methodWithArgs<This, Args, Return>,
context: ClassMethodDecoratorContext<This, methodWithArgs<This, Args, Return>>) {
const methodName = String(context.name);
function replacementMethod(this: This, ...args: Args) {
const result = originalMethod.call(this, ...args);
console.info(`${message} ${methodName}(${args}) => ${result}`);
return result;
}
return replacementMethod;
}
}
Nous notons ici que nous exécutons notre méthode hurler grâce à :
originalMethod.call(this, ...args);
Ce qui nous permet d’écrire dans la console, avant et après.
Passons maintenant à un décorateur de champ / property.
Décorateur de validation
Ici, nous allons utiliser des meta-données. Pour se faire, nous allons utiliser la librairie : “reflect-metadata”.
Note : afin que tout fonctionne, ajoute bien dans ton tsconfig cette ligne, pour prendre en compte le typage / compilation de la librairie :
"compilerOptions": {
"target": "es6",
"types": ["reflect-metadata"],
...
}
Puis dans ton fichier ts de ton decorateur, pense à ajouter :
import "reflect-metadata";
Et c’est parti. !
Première étape
Créons le decorator qui va ajouter une metadata ‘Required’ sur notre champs / property.
export function required<T extends Object>(
target: undefined,
context: ClassFieldDecoratorContext<T, string | undefined>
) {
return function (this: T, value: string | undefined) {
const requiredMetadataKey = 'required';
const propertyKey = String(context.name);
Reflect.defineMetadata(
requiredMetadataKey,
true,
this,
propertyKey
);
return value;
};
}
Notons ici que nous appliquons l’ajout de la metada sur la property context.name, donc notre champ de classe.
Deuxième étape
Créons une méthode qui va valider si notre champ de notre objet est valide ou non (vide ou renseigné) :
export function validate<T extends Object>(object: T): void {
const requiredMetadataKey = 'required';
for(const key in object) {
if (object.hasOwnProperty(key)) {
const isRequired = Reflect.getMetadata(requiredMetadataKey, object, key);
if (isRequired && !object[key]) {
throw new Error(`${key} is required`);
}
}
}
}
Note: ici nous avons créé un moteur qui ne fonctionne que pour required : il va être intéressant par la suite de créer des validateur custom par decorateur : par exemple, un RequiredValidator.
Nous parcourons la list des propriétés / champs de notre objet, et nous cherchons à savoir si une valeur est undefined ou null.
Si c’est le cas, nous délenchons une erreur.
Note: on pourrait aussi retourner une liste d’erreur dans un dictionnaire.
Utilisation de notre moteur
Ajoutons une classe et instancions la, avant de valider l’objet :
import { logged, required } from './decorators';
export class Wookiee {
@logged('Wookiee')
hurler(): void {
console.log('Rowaaaaa');
}
@required
name: string|undefined = '';
}
let wookiee = new Wookiee();
wookiee.hurler();
wookiee.name = undefined;
validate(wookiee);
Et hop, une belle erreur :
Conclusion
C’est un vrai plaisir de coder des decorateurs, pour ajouter de l’AOP à notre code (Aspect Oriented Programation).
La façon de faire des nouveaux decorateurs ne changent pas trop dans leur appel, ni dans leur création.
Je t’invite vraiment à en mettre le plus possible dans ton code ! 🙂
Happy coding
Stay curious !