With TypeScript 1.5, the language introduced decorators alongside the EcmaScript 6 module syntax and other enhancements. Decorators are functions that can be attached to various language constructs, including functions, classes, and properties. When a script starts, JavaScript invokes the decorator functions and passes the annotated constructs to them. The decorator can then extend or replace those constructs. An overview of this feature, which will also be used in Angular 2, is available at [1] and [2].
Using the current alpha version of TypeScript, this post demonstrates decorators through a simple validation framework. The goal is to define validation rules for classes and their properties by means of annotations. The following example shows such a class:
@Validate(h => h.minPrice <= h.maxPrice, "min < max")
class Hotel {
@Required
name: string;
@MinValue(0, "Min: 3")
@MaxValue(7, "Max: 7")
ranking: number;
minPrice: number;
maxPrice: number;
}
In the example, the @Required annotation marks a mandatory field, while @MinValue and @MaxValue enforce lower and upper bounds, respectively. These bounds and the corresponding error messages are supplied as parameters. Unlike the others, the @Validate annotation targets a class. It accepts a lambda expression for the validation logic along with an error message.
To implement the validation, every object being validated should carry an array called __validators filled with validation functions. Each validation function checks one aspect of the object and returns an error message on failure; otherwise, it returns null. For this purpose, the example relies on a helper function named addValidator:
function addValidator(target, fn, errorMessage) {
var validationFn = (obj) => !fn(obj) ? errorMessage : null;
if (!target.__validators) {
target.__validators = [];
}
target.__validators.push(validationFn);
}
This function takes the object to validate, a validation function, and an optional error message. The supplied function returns true when the validation succeeds and false otherwise. From that, addValidator constructs a validation function that yields the error message in case of failure and stores it in the validationFn variable. Next, it checks whether the object already has a __validators array and adds one if needed. Finally, the validation function is pushed into that array.
A first version of the Required decorator is shown in the next example. Since this decorator applies to properties, it receives the prototype and the property name by definition. It creates a function that validates a required field by checking whether the value is null or undefined. The decorator then registers this function along with a simple error message using addValidator.
function Required(target, name) {
var fn = (obj) => obj[name] !== null && typeof obj[name] !== "undefined";
var errorMessage = name + " is required!";
addValidator(target, fn, errorMessage);
}
One drawback of the last example is that the error message is hardcoded. This can be avoided by introducing a function that produces the decorator. Such factory functions can appear in source code just like decorators. The next example illustrates this approach. It shows two factory functions that accept validation parameters and return a decorator function with the usual signature:
function MinValue(min, errorMessage) {
return function (target, name) {
var val = (obj) => obj[name] >= min;
addValidator(target, val, errorMessage);
};
}
function MaxValue(max, errorMessage) {
return function (target, name) {
var val = (obj) => obj[name] <= max;
addValidator(target, val, errorMessage);
};
}
The factory function for the Validate annotation is built in a similar way:
function Validate(fn, errorMessage) {
return function (target) {
addValidator(target.prototype, (obj) => fn(obj), errorMessage);
};
}
It takes a lambda expression and an error message for validation. The resulting decorator receives the affected construct as usual. Since Validate is meant for classes, this construct is not an object but the class itself, i.e., its constructor function. For the same reason, a parameter for the property name is not needed. To ensure that the validation function is added to all objects created from the class, the example passes the class's prototype to addValidator.
What remains is code that invokes all validation functions of an object and collects the resulting error messages. The next example shows a class Validator that handles this task:
class Validator {
static validate(obj) {
var errMessages = [];
if (!obj || !obj.__validators) return errMessages;
for (var fn of obj.__validators) {
var errMessage = fn();
if (errMessage) {
errMessages.push(errMessage);
}
}
return errMessages;
}
}
To test the implementation, the class shown at the beginning can be instantiated and passed to Validator.validate:
var hotel = new Hotel();
hotel.name = null;
hotel.ranking = 99;
hotel.minPrice = 120;
hotel.maxPrice = 80;
var err = Validator.validate(hotel);
if (err.length > 0) {
alert(err.join("\n"));
}
else {
alert("No errors!");
}
Links
