TypeScript Metadata Decorators can make life easier for Angular developers. Learn how to take an advantage of this ground-breaking ECMAScript proposed feature.
When creating models, there are times when the ability to detail properties could bring so much more versatility and code reusability to the source. We might want to describe a property to add formatting, restrictions or even have different components behave accordingly to specific details.
Other languages such as C# have Attributes that already do what we are going to go through in this post. What we will end up with is the ability to associate metadata with class properties through a decorator factory.
Take the following model:
class UserModel {
username: string;
password: string;
email: string;
age: number;
name: string;
}
Assuming that we have a component somewhere that takes this model and creates a form, our goal is to describe our model so that any other component who consumes it may know how to handle its fields and behave/present them accordingly.
To augment the model parameters, we will be using the Metadata Proposal polyfill. Reflect metadata allows us to get and set key-value pairs from properties, classes, and methods.
Install it in your project using npm install reflect-metadata and then include it with import “reflect-metadata”;.
We also have to add the “experimentalDecorators”: true and the emitDecoratorMetadata”: true properties in our tsconfig.json file.
Just by doing so, you will now have access to reflect metadata together with the @Reflect decorator. You can now apply it to the model above like so:
class UserModel {
@Reflect.metadata(‘attribute:type’, text)
@Reflect.metadata(‘attribute:icon’, ‘fa-user’)
username: string;
@Reflect.metadata(‘attribute:type’, ‘password’)
@Reflect.metadata(‘attribute:icon’, ‘fa-key’)
password: string;
(...)
}
Reflect Metadata methods will store the key value pairs in the objects prototype as a Map, in a new [[Metadata]] internal property. Most Objects in JavaScript are instances of Object. This inheritance is created via prototype chain, where every object has a __proto__ private property that will reference another object above it until you reach the Object, who has a null __proto__ reference. That will be the highest level in the chain.
Reflect also provides getters, such as the getMetadata method, that will retrieve the value associated with a respective key.
Now that we know how to set and get metadata from our model properties, we can create a decorator to make the setting easier and typed thanks to Typescripts parameter decorators.
Let’s start by creating an interface for our attribute metadata.
interface IAttributeProperties {
icon?: string;
type?: AttributeType;
isEditable?: boolean;
isVisible?: boolean;
}
enum AttributeType {
Text,
Date,
Number,
Password
}
Now that we have a type for our attributes, let’s implement our property attribute decorator factory:
export const ATTRIBUTE_PREFIX = 'attribute:';
/**
* Adds attribute metadata to a property
* @param {IAttributeProperties} attributes
* @returns {(target: any, propertyKey: string) => void}
* @constructor
*/
export function Attribute(attributes: IAttributeProperties) {
return (target: object, propertyKey: string) => {
if (attributes !== undefined && attributes !== null) {
Object.keys(attributes).forEach(key => {
Reflect.defineMetadata(`${ATTRIBUTE_PREFIX}${key}`, attributes[key], target, propertyKey);
});
}
};
}
The decorator above will receive an implementation of our IAttributeProperties interface and will then add it to a property metadata.
Now our model will look like:
class UserModel {
@Attribute({
icon: ‘fa-user’,
type: AttributeType.Text
})
username: string;
@Attribute({
icon: ‘fa-key’,
type: AttributeType.Password
})
password: string;
@Attribute({
icon: ‘fa-at’,
type: AttributeType.Text,
isVisible: false
})
email: string;
(...)
}
Calling Reflect.getMetadata(‘attribute:icon’, obj, ‘password’) from the instanced class above would return us the ‘fa-key’ icon.
Now we can describe our model with typed annotations that will be passed together with it during its instance lifetime.
This was just an example of a common model use case. We can also pass functions and have them called later on, as this opens a lot of possibilities for enhancing code (constructors, properties and methods) with declarative information.
Metadata Maps are a game changer for JavaScript, as this feature isn’t exclusive to TypeScript and most code bases can take advantage of it. It’s currently in the proposal status and we will need to make happen with the polyfill for now.
Written by Nuno Rocha | Senior Developer at Cleverti