Generate an HTML form from an object in TypeScript

In the previous post about TypeScript decorators, I used decorators to quickly add validation rules. In this post, we'll use another features of the decorators. TypeScript can automatically add the type of the property to the metadata. Let's see how we can use this information and other custom attributes to automatically generate a form from a class.

The idea is to be able to use the following code:

class Person {
    @editable()
    @displayName("First Name")
    public firstName: string;
    @editable()
    @displayName("Last Name")
    public lastName: string;
    @editable()
    public dateOfBirth: Date;
    @editable()
    public size: number;
}

var author = new Person();
author.firstName = 'Gérald';
generateForm(document.body, author);

Generated form

Let's configure TypeScript to enable decorators and metadata.

  • experimentalDecorators allows to use the decorators in your code.
  • emitDecoratorMetadata instructs the compiler to add a metadata design:type for each property with a decorator.

The project.json should contains the 2 attributes:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
  }
}

The compiler will translate the TypeScript class to JavaScript, and decorates the properties with the required, displayName and design types. Here's an extract of the generated code:

Person = /** @class */ (function () {
    function Person() {
    }
    __decorate([
        editable(),
        displayName("First Name"),
        __metadata("design:type", String) // Added by emitDecoratorMetadata: true
    ], Person.prototype, "firstName", void 0);
    // ...
    return Person;
}());

Let's declare the editable and displayName decorators. You can look at the previous post to get a better understanding of TypeScript decorators.

function editable(target: any, propertyKey: string) {
    let properties: string[] = Reflect.getMetadata("editableProperties", target) || [];
    if (properties.indexOf(propertyKey) < 0) {
        properties.push(propertyKey);
    }

    Reflect.defineMetadata("editableProperties", properties, target);
}

function displayName(name: string) {
    return function (target: any, propertyKey: string) {
        Reflect.defineMetadata("displayName", name, target);
    }
}

Now, you can use the metadata to generate the form. You have to find the editable properties, get their display name to create the label, and their type to create the right input type. For instance, you have to use <input type="number"/> for a property of type number, and <input type="checkbox"/> for a property of type boolean. Then, you have to bind the inputs to the model, so changes in the UI are propragated to the model. The input event should be ok for that.

function generateForm(parentElement: HTMLElement, obj: any) {
    const form = document.createElement("form");

    let properties: string[] = Reflect.getMetadata("editableProperties", obj) || [];
    for (let property of properties) {
        const dataType = Reflect.getMetadata("design:type", obj, property) || property;
        const displayName = Reflect.getMetadata("displayName", obj, property) || property;

        // create the label
        const label = document.createElement("label");
        label.textContent = displayName;
        label.htmlFor = property;
        form.appendChild(label);

        // Create the input
        const input = document.createElement("input");
        input.id = property;
        if (dataType === String) {
            input.type = "text";
            input.addEventListener("input", e => obj[property] = input.value);
        } else if (dataType === Date) {
            input.type = "date";
            input.addEventListener("input", e => obj[property] = input.valueAsDate);
        } else if (dataType === Number) {
            input.type = "number";
            input.addEventListener("input", e => obj[property] = input.valueAsNumber);
        } else if (dataType === Boolean) {
            input.type = "checkbox";
            input.addEventListener("input", e => obj[property] = input.checked);
        }

        form.appendChild(input);
    }

    parentElement.appendChild(form);
}

You can now use the code at the beginning of the post, and it should work:

var author = new Person();
author.firstName = 'Gérald';
generateForm(document.body, author);

Conclusion

Decorators and metadata are very similar to what C# provides out of the box with attributes. It allows you to enrich the code with additional information, and get those information at runtime. In the previous post, I created a generic validation system. Today, I write a generic form generator in a few lines of code. They are lots of possibilities! If you are familiar with reflection in C#, I think you already have hundreds of ideas 😃

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 attach 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 to use decorators for new scenario. If you look at it, it really similar to C#.

Starting a TypeScript project with Visual Studio Code

Visual Studio Code is a very great IDE for TypeScript. While VS Code is a lightweight source editor, it is very powerful. It supports lots of languages such as C++, C#, Java, Python, PHP, Go, but some of them have a better integration. This is the case for TypeScript. VS Code allows you to write TypeScript code with autocompletion, refactoring, error reporting, quick fixes, automatic build, debugging, etc. In this post, we'll see how to set up your first TypeScript project with VS Code.

If you are not familiar with VS Code, here's a quick description from the documentation:

Install Visual Studio Code and TypeScript

  1. Install VS Code: https://code.visualstudio.com/

  1. Install NodeJS : https://nodejs.org/
  2. Install TypeScript using the following command line:
npm install -g typescript
  1. Check TypeScript is installed
tsc --version

Create a project

  1. Create a new folder that will contains the sources
  2. Create a tsconfig.json with the following content (you can use the command tsc --init):
{
    "compilerOptions": {
        "target": "es5"
    }
}
  1. Create an src folder
  2. Add an empty TypeScript file in the src folder

The file structure should be like:

MyTypeScriptProject
├── src
│   └── main.ts
└── tsconfig.json

Compile the project

Open the command palette using CTRL+SHIFT+P, and type "Tasks":

Select "Configure Default Build Task", then select "tsc:watch"

This will create a new file .vscode/tasks.json with the following content:

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "type": "typescript",
            "tsconfig": "tsconfig.json",
            "option": "watch",
            "problemMatcher": [
                "$tsc-watch"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

You can now build your TypeScript files by pressing CTRL+SHIFT+B. After the compilation succeed, you should see a new file main.js in the explorer:

The task is a watch task, so TypeScript will run automatically when you change a TypeScript file.

Hide the JavaScript file from the Explorer

After compiling the TypeScript files, you'll have lots of ts and js files in the explorer of VS Code. As you'll never edit the JS files, you can simply hide them in the Explorer.

  1. Open the settings: File -> Preferences -> Settings
  2. Select "Workspace settings" in the right panel
  3. Add the following json
{
    "files.exclude": {
        "**/*.js": true
    }
}

The workspace settings are saved in .vscode/settings.json, so this will apply only for the current project.

Conclusion

You are now ready to use Visual Studio Code for your TypeScript projects

Import polyfill using dynamic imports in TypeScript

TypeScript 2.4 has introduced Dynamic imports. In a previous, we have seen how to use static imports in TypeScript. Static import allow to use a module. You import all the modules needed at the beginning of the TypeScript file and use them later:

import { ZipCodeValidator } from "./ZipCodeValidator";
const myValidator = new ZipCodeValidator();

Dynamic imports on the other hand are not declared at the beggining of the file, but right before their usage. This is very useful to load a module only when you really need it. For instance, if a feature is not commonly use, you can load the code of that feature only when the user starts using it. This way, when the page loads, the user don't need to download all the code, so it reduces the time to load the page.

if (condition) {
    let zipCodeValidator = await import("./ZipCodeValidator");
    let myValidator = new zipCodeValidator();
}

What is a polyfill?

From wikipedia:

In web development, a polyfill is code that implements a feature on web browsers that do not support the feature. Most often, it refers to a JavaScript library that implements an HTML5 web standard, either an established standard (supported by some browsers) on older browsers, or a proposed standard (not supported by any browsers) on existing browsers. Formally, "a polyfill is a shim for a browser API".

Polyfills allow web developers to use an API regardless of whether it is supported by a browser or not, and usually with minimal overhead. Typically they first check if a browser supports an API, and use it if available, otherwise using their own implementation.

For instance, String.includes is not available in Internet Explorer 11. This is a very convenient function, so I want to be able to use it. The idea is to run the polyfill before the rest of the code, so you can use the function. You can find a polyfill on MDN.

// Test if String.includes is available
if (!String.prototype.includes) {

    // Add the find method to the prototype
    String.prototype.includes = function(search, start) {
        'use strict';
        if (typeof start !== 'number') {
            start = 0;
        }

        if (start + search.length > this.length) {
            return false;
        } else {
            return this.indexOf(search,start) !== -1;
        }
    };
}

Load polyfills dynamically

The previous polyfill is not very long. However, if you need to include ten polyfills or a much longer one, you'll increase the size of the JS, and your web site will be slower. Plus, String.includes is well supported by the current browsers. You can check that information on caniuse:

So, you need this polyfill for just a few users. You don't want to impact the vast majority of your audiance. So, why not loading this polyfill dynamically. This is where dynamic import is useful. Indeed, you can create the polyfill as a module and import it only when needed.

Here's the structure I use:

src
├── polyfills
│   ├── array.from.js
│   ├── array.prototype.find.js
│   ├── string.prototype.includes.js
│   └── ...
└── main.ts

Polyfills are often paste from the internet, so you can use JavaScript or TypeScript depending on the source.

Then, you can import them dynamically if needed in the main file:

function importPolyfills() {
    let promises = [];
    if (!String.prototype.includes) {
        promises.push(import("./polyfills/string.prototype.includes"));
    }

    if (!Array.from) {
        promises.push(import("./polyfills/array.from"));
    }

    if (!Array.prototype.find) {
        promises.push(import("./polyfills/array.prototype.find"));
    }

    // ...

    return Promise.all(promises);
}

importPolyfills().then(() => {
    console.log("meziantou".includes("an"));
});

This way, only the users who use a browser that don't support the feature you want will actually download the polyfill. For them, the loading time will increase a little. But, for the vast majorty of the users, it will reduce the loading time. And when a browser releases the functionality, the polyfill won't be downloaded anymore. So, you can measure how many people download the polyfill file and check if it is actually needed.

How to migrate from JavaScript to TypeScript?

After the previous posts about TypeScript, I'm sure you'd like to migrate your application from JavaScript to TypeScript. If you haven't read them yet, take a few minutes:

The migration from JavaScript to TypeScript is not a very complicated process. But there are some steps to follow to achieve it correctly without spending too much time.

Learn the TypeScript language

Of course, you have to start learning the language. If you are already a JavaScript developer, learning TypeScript is easy.

Here's a few good links:

You'll also find useful resources on this blog 😃

Install TypeScript

If you are using Visual Studio, the TypeScript compiler is already installed. In other cases, you must install it.

npm install typescript
.\node_modules\.bin\tsc --version

or you can install it globally

npm install -g typescript
tsc --version

Add the TypeScript configuration file to your project

The configuration file indicates to the TypeScript compiler the options to use to compile the project. This is also the root of the TypeScript project.

Your solution should look like:

Root
├── src
│   ├── file1.js
│   └── file2.js
├── built
└── tsconfig.json

The tsconfig.json at the root of the project contains:

{
  "compilerOptions": {
    "target": "es5",
    "allowJs": true,
    "outDir": "./built"
  },
  "include": [
      "./src/**/*"
  ]
}

The file indicates the TypeScript compiler to accept JavaScript files, so you don't need to change all your code at once. With this option, you can convert your files one by one. The outDir indicates where TypeScript outputs the files after the compilation. The include options indicates where are located the source files.

By default, the compiler won't analyse the error in the JS files. You can start getting some of the bemefits of TypeScript by indicates the compiler to check your JS files using "checkJs": true:

{
  "compilerOptions": {
    "target": "es5",
    "allowJs": true,
    "checkJs": true,
    "outDir": "./built"
  },
  "include": [
      "./src/**/*"
  ]
}

The compiler will use the information it can gather from your files to indicate errors (documentation). For instance, it can use JSDoc to find types or resolve require("...") statements. So, You'll get some of the TypeScript advantages immediately. You can also set some compiler options such as noImplicitReturns: true, noFallthroughCasesInSwitch: false, allowUnreachableCode: false or allowUnusedLabels: false.

/** @type {string} */
var author;

author = "meziantou"; // OK
author = false;       // Error: boolean is not assignable to string

Convert files to TypeScript and fix common errors

It's time to start using TypeScript. The only change required is to change the extension a the files from .js to .ts. You don't need to migrate all files at once. You can migrate files one by one. If you have a large code base, you'll probably get hundreds of errors. So, converting files one by one will allow to handle them more easily.

Some of valid JavaScript will stop working in TypeScript. Mainly because of the type checker. Here's a few errors you may encounter.

Sequentially Added Properties

The following code is very common in JavaScript:

var author = {};
author.nickname = "Meziantou";
author.fullName = "Gérald Barré";

In TypeScript the type of author is an empty object. So nickname and fullname doesn't exist and are not assignable. The solution is to move the declarations into the object creation:

var author = {
    nickname = "Meziantou",
    fullName = "Gérald Barré"
};

The other solution is to create a type for the author variable, so the compiler knows the list of properties available.

interface Author { nickname: string; fullName: string }

var author = {} as Author;
author.nickname = "Meziantou";
author.fullName = "Gérald Barré";

The latest option, which is not the best, is to indicate the compiler to not check the usages of the author variable using the type any:

var author: any = {};
author.nickname = "Meziantou";
author.fullName = "Gérald Barré";

Using a well-know library

TypeScript must know the types of every objects to compile a file. So, if you are using a library such as JQuery, loadash, etc., you have to tell TypeScript what the library contains. Fortunately, the TypeScript definitions of thousands of lib are available via npm. For instance, if you use jquery, you can install the definition using

npm install @types/jquery

You'll find the complete list of supported libraries on npm: https://www.npmjs.com/~types

Using others libraries

If the types are not available, you have 2 options:

  • Create the definition of the methods/objects
  • Create a dummy declaration

For instance, you use a lib that exposes a function add(a, b) and an object _, you can create the following dummy declaration, so the compiler is happy. The any type indicates to the compiler to not check the value.

declare function add(...args: any[]): any;
declare var _: any;

add(1, 2);
_.filter(characters, { 'age': 36 });

With these definitions, anything after _ will be valid. While this is not the philosophy of TypeScript, it's better to have 80% of the code checked instead of 0%. So, it's a good start. Later, you can write a better definition such as:

declare function add(a: number, b: number): number;
declare var _: Underscore;

interface Underscore {
    filter(obj: any[], filter: { [name: string]: any });
}

External modules (require, define)

If you are using modules with commonjs, requirejs or amd, you can use a dummy declaration:

declare function require(path: string): any;
declare function define(...args: any[]): any;

However, you should use the TypeScript syntax. This is more convenient and you'll get the typing information. First, change the module option in the tsconfig.json file:

{
  "compilerOptions": {
    "module": "commonjs" // Set the kind of module you are using
  }
}

Then change your import statements:

// JavaScript
var math = require("./math");

// TypeScript (one of the following)
import math = require("./math");
math.add(1, 2);

import { add } from "./math";
add(1, 2);

You can also replace the exports with the new syntax

// JavaScript
module.exports.add = function(a, b) {
    return a + b;
}

// TypeScript
export function add(a, b) {
    return a + b;
}

Use new syntaxes

You know have a valid TypeScript project. You can start using the new TypeScript language features. This should help you writing more readable code. For instance, you can:

  • Replace immediately-invoked function expressions with namespace/module
  • Use classes when possible
  • Replace var with let or const
  • Replace magic numbers/strings with enumeration
  • Replace require() with import (check the previous section)
  • Use async await to simplify the usage of asynchronous code
  • Replace property names with nameof equivalent

Some tools such as Resharper allows to replace common JS syntax with a TypeScript equivalent: https://blog.jetbrains.com/dotnet/2015/02/05/ways-and-advantages-of-migrating-javascript-code-to-typescript/

Enable compiler checks to find more errors

Finally, you should change the compiler options to enable more and more checks. This will help you find more errors at compile time. Some of them such as strictNullChecks will generates lots of errors (maybe thousands). So, you should enable these option one by one and fix the new errors.

{
    "compilerOptions": {
        "allowUnreachableCode": false,
        "allowUnusedLabels": false,
        "alwaysStrict": true,
        "noImplicitAny": true,
        "noImplicitReturns": true,
        "noImplicitThis": true,
        "noFallthroughCasesInSwitch": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "suppressExcessPropertyErrors": false,
        "suppressImplicitAnyIndexErrors": false,
        "strictFunctionTypes": true,
        "strictNullChecks": true,
        "strictPropertyInitialization": true,
        "checkJs": true // If you are still using some js files
    }
}

You can read more about all these options in a previous post: Detect common JavaScript errors with TypeScript.