Angular 2: Convert input values to the correct type with a TypeScript property decorator

February 24, 2016, (updated on March 1, 2016), 8 comments, Software Development

I recently played with Angular 2 and found an uncomfortable problem: Declaring an input property with a TypeScript type does not guarantee that the input value will always be of this type. This is because the Angular framework may update an input with a wrongly typed value and thus is able to “circumvent” the TypeScript type system. As a result, you may end up with strange and unexpected runtime errors.

Let’s have a look at this simple Angular component:

@Component({ selector: "my-component" })
export class MyComponent {
    @Input()
    enabled = true; 

    showValue() {
        alert(typeof this.enabled);   
    }
}

As shown in the HTML below, if the enabled input is set as an HTML attribute, the type of the value will be a string, even if the property is defined as boolean. When the showValue method is called, the text in the alert box will be string.

<my-component enabled="false"></my-component>

To solve this problem I wrote a TypeScript property decorator which intercepts the property setter and converts the value if it has the wrong type.

First, I declared the most common converters:

export var StringConverter = (value: any) => {
    if (value === null || value === undefined || typeof value === "string")
        return value;

    return value.toString();
}

export var BooleanConverter = (value: any) => {
    if (value === null || value === undefined || typeof value === "boolean")
        return value;

    return value.toString() === "true";
}

export var NumberConverter = (value: any) => {
    if (value === null || value === undefined || typeof value === "number")
        return value;

    return parseFloat(value.toString());
}

The implemented property decorator InputConverter is essentially a factory method which creates a decorator function. This function is called for each property which has a decorator annotation (i.e. @InputConverter(BooleanConverter)).

export function InputConverter(converter?: (value: any) => any) {
    return (target: Object, key: string) => {
        if (converter === undefined) {
            var metadata = (<any>Reflect).getMetadata("design:type", target, key);
            if (metadata === undefined || metadata === null)
                throw new Error("The reflection metadata could not be found.");

            if (metadata.name === "String")
                converter = StringConverter;
            else if (metadata.name === "Boolean")
                converter = BooleanConverter;
            else if (metadata.name === "Number")
                converter = NumberConverter;
            else
                throw new Error("There is no converter for the given property type '" + metadata.name + "'.");
        }

        var definition = Object.getOwnPropertyDescriptor(target, key);
        if (definition) {
            Object.defineProperty(target, key, {
                get: definition.get,
                set: newValue => {
                    definition.set(converter(newValue));
                },
                enumerable: true,
                configurable: true
            });
        } else {
            Object.defineProperty(target, key, {
                get: function () {
                    return this["__" + key];
                },
                set: function (newValue) {
                    this["__" + key] = converter(newValue);
                },
                enumerable: true,
                configurable: true
            });
        }
    };
}

If the converter is undefined, then the function tries to use TypeScript reflection to find the correct converter for the properties type. In order to use this feature, you need to enable the option emitDecoratorMetadata in the TypeScript compiler (or add the TypeScriptEmitDecoratorMetadata tag to your .csproj) and add a polyfill for the reflection API (for example using es6-shim).

The method also checks if a property descriptor is already available and calls its setter and getter to read and write values. If the property is not yet initialized, then the values are read and written from an instance field.

Now, the property decorator can be used very easily: Import the decorator function and create a property annotation with the desired converter type:

export class MyComponent {
    @Input()
    @InputConverter(BooleanConverter)
    enabled = true; 
}

The converter is automatically set, if reflection is enabled. In the following code, the enabled property is converted using the BooleanConverter:

export class MyComponent {
    @Input()
    @InputConverter()
    enabled: boolean = true; 
}

I personally think this feature should be part of Angular’s Input decorator because every TypeScript developer expects that the type of input values are always correct…

What do you think? Should this feature be baked into the Angular 2 core framework?

Tweet about this on TwitterShare on FacebookEmail this to someoneShare on TumblrShare on LinkedIn

Tags: , , , ,

8 responses to “Angular 2: Convert input values to the correct type with a TypeScript property decorator”

  1. Frank says:

    Good source. I would appreciate some examples as well. Not sure what is wrong but when I tried to create my own decorator and trying to access the the Object the

    this [key] is undefined as well as when I call Object.getOwnPropertyDescriptor(target, key); I get nothing.
    not sure what I am doing wrong so some plunkr would be great inspiration.

    • Rico Suter says:

      Just tried it with the latest Angular 2 release and it still works for me.

    • Rico Suter says:

      Thanks for your comment. As soon as I find the time, I’ll add a sample project or a plunkr. Maybe it is not working with the latest Angular 2 release?

      • Frank says:

        Not sure I just started to debug it. but simple straigh forward fails. Its so simple that nothing cant go wrong.

        export function Test() { 
            return function (target: Object, key: string) { 
                var _val = this[key]; console.log('target: ' + _val);
                var ownPropertyDescriptor = Object.getOwnPropertyDescriptor(target, key); 
                console.log('target2: ' + ownPropertyDescriptor);
            }
        } 
        
  2. Isidoro Aguilera says:

    You must put inputs between [] to work properly.

    Instead of enabled=”false” write [enabled]=”false”

    Then angular2 sets a boolean value without the need of the custom decorator.

  3. Prasad Kumbhare says:

    Thanks so much.. ran into same issue.. appreciate if you could provide a plunkr or github source code. Many Thanks Prasad

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax