Validation made easy with decorators
Before reading this post, you should read the previous post about Aspect Oriented Programming in TypeScript. It will help you understand the TypeScript decorators.
The idea of this post is to write a class and use decorators to set the validation rules on attributes:
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 the validation rules to the associate properties. Then, you can query these metadata to run the rules. TypeScript supports the Metadata Reflection API. This API defines a few methods. Here's are the ones we'll use:
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 a 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:
npm install reflect-metadata
Then, let's create a function to add the metadata to the property and class:
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:
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:
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 allows using of decorators for new scenarios. If you look at it, it is similar to C#.
Do you have a question or a suggestion about this post? Contact me!