Before reading this post, you should read the previous post about Aspect Oriented Programming in TypeScript. It will help you understand the TypeScript decorators.
This post shows how to write a class and use decorators to define validation rules on properties:
TypeScript
class Customer {
@required
public firstName: string;
@required
public lastName: string;
}
var customer = new Customer();
customer.firstName = 'Gérald';
validate(customer); // 'lastName' is required
The solution is to use decorators and metadata to attach validation rules to the associated properties. You can then query this metadata to run the rules. TypeScript supports the Metadata Reflection API. This API defines a few methods. Here are the ones we'll use:
TypeScript
namespace Reflect {
// Set metadata
// Reflect.defineMetadata("custom:annotation", value, Customer);
function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
// Reflect.defineMetadata("custom:annotation", value, Customer.prototype, "firstName");
function defineMetadata(metadataKey: any, metadataValue: any, target: Object, propertyKey: string | symbol): void;
// Get metadata
// Reflect.getMetadata("custom:annotation", Customer);
function getMetadata(metadataKey: any, target: Object): any;
// Reflect.getMetadata("custom:annotation", Customer.prototype, "firstName");
function getMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): any;
}
The following code attaches metadata named "validation" to the property and the class. The metadata for the property is a list of ValidationRule. For the class, we attach the list of properties to validate.
First, you need to install the metadata polyfill:
Shell
npm install reflect-metadata
Then, let's create a function to add the metadata to the property and class:
TypeScript
import "reflect-metadata";
function addValidationRule(target: any, propertyKey: string, rule: IValidationRule) {
let rules: IValidationRule[] = Reflect.getMetadata("validation", target, propertyKey) || [];
rules.push(rule);
let properties: string[] = Reflect.getMetadata("validation", target) || [];
if (properties.indexOf(propertyKey) < 0) {
properties.push(propertyKey);
}
Reflect.defineMetadata("validation", properties, target);
Reflect.defineMetadata("validation", rules, target, propertyKey);
}
Let's implement the required validation rule:
TypeScript
interface IValidationRule {
evaluate(target: any, value: any, key: string): string | null;
}
class RequiredValidationRule implements IValidationRule {
static instance = new RequiredValidationRule();
evaluate(target: any, value: any, key: string): string | null {
if (value) {
return null;
}
return `${key} is required`;
}
}
function required(target: any, propertyKey: string) {
addValidationRule(target, propertyKey, RequiredValidationRule.instance);
}
Finally, you can validate an object using the metadata:
TypeScript
function validate(target: any) {
// Get the list of properties to validate
const keys = Reflect.getMetadata("validation", target) as string[];
let errorMessages: string[] = [];
if (Array.isArray(keys)) {
for (const key of keys) {
const rules = Reflect.getMetadata("validation", target, key) as IValidationRule[];
if (!Array.isArray(rules)) {
continue;
}
for (const rule of rules) {
const error = rule.evaluate(target, target[key], key);
if (error) {
errorMessages.push(error);
}
}
}
}
return errorMessages;
}
function isValid(target: any) {
const validationResult = validate(target);
return validationResult.length === 0;
}
#Conclusion
In the previous post, I showed how to use decorators to change the default behavior of a method or class. Metadata opens up new scenarios for decorators, similar to what you may recognize from C#.
Do you have a question or a suggestion about this post? Contact me!