typescript

An in-depth understanding of the implementation of TypeScript’s modifiers, which make it possible for JavaScript to implement reflection and dependency injection.

The tutorial is divided into four main parts

  • Part 1: Method modifiers
  • Part I: Property modifiers & class modifiers
  • Part III: Parameter Modifiers & Modifier Factories
  • Part IV: Type serialization & metadata reflection API

In this article we will learn

  • Why our JavaScript needs reflection
  • The metadata reflection API
  • Basic type serialization
  • Serialization of complex types

Why is reflection needed in JavaScript?

Reflection is often used to describe code or review other code in the same system

Reflection is very useful in composition, dependency injection, runtime type assertion, testing

As our javascript applications get bigger and bigger, we start to need tools (like dependency inversion control, runtime type assertions) to manage the growing complexity of our applications. The problem now is that JavaScript does not have reflection and these tools or features will not be implemented, but some powerful programming languages implement reflection like C# or Java.

A powerful reflection API would allow us to test an unknown object at runtime and find all the information about it. We expect to find the following information

  • The name of the entity
  • The type of the entity
  • That interface is implemented by the entity
  • the name and type of the entity’s properties
  • the name and type of the entity’s constructor parameters

In JavaScript we can use that function Object.getOwnPropertyDescriptor() or Object.keys() to find some information about the entity, but we need reflection to implement more powerful tools.

However, these situations will change, because TypeScript starts to support some reflection features, let’s take a look at them.

Metadata Reflection API

The TypeScript team developers used the Polyfilli shim to add the reflection API to ES7. The TypeScript compiler can now emit some serialized design-time metadata types for decorators.

We can use the metadata reflection API by using the reflect-metadata package.

1
npm install reflect-metadata;

We have to use TypeScript version 1.5 onwards and set the compiler logo emitDecoratorMetadata to true, we also need to include the reflect-metadata.d.ts file and load Reflect.js.

We next implement our own modifier and use the reflect metadata design key, but for now there are three types of design keys available

  • type of metadatau using the metadata key “design:type”.
  • type of parameters metadata use metadata key “design:paramtypes”
  • return type metadata use metadata key “design:returntype”

Let’s see a few examples.

Get the type metadata of an attribute using the reflect metadata API

Let’s declare an attribute modifier.

1
2
3
4
function logType(target : any, key : string) {
    var t = Reflect.getMetadata("design:type", target, key);
    console.log(`${key} type: ${t.name}`);
}

We can apply it to an attribute of a class.

1
2
3
4
class Demo{ 
    @logType // apply property decorator 应用属性修饰器
    public attr1 : string;
}

The above console will output the following.

1
attr1 type: String

Get metadata of parameter type using reflect metadata API

Let’s declare a parameter modifier.

1
2
3
4
5
function logParamTypes(target : any, key : string) {
    var types = Reflect.getMetadata("design:paramtypes", target, key);
    var s = types.map(a => a.name).join();
    console.log(`${key} param types: ${s}`);
}  

We apply it to a method of a class and get information about the type of the parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Foo {}
interface IFoo {}

class Demo{ 
    @logParameters // apply parameter decorator 应用参数修饰器
    doSomething(
    param1 : string,
    param2 : number,
    param3 : Foo,
    param4 : { test : string },
    param5 : IFoo,
    param6 : Function,
    param7 : (a : number) => void,
    ) : number { 
        return 1
    }
}

The console in the above example will output the following.

1
doSomething param types: String, Number, Foo, Object, Object, Function, Function

Get the metadata of the return type using the reflect metadata API

We can also get information about the return type of the method using the “design:returntype” metadata key

1
Reflect.getMetadata("design:returntype", target, key);

Serialization of base types

Let’s take a look at the design:paramtypes example above again. Notice that the interface IFoo and the literal object { test : string} have been serialized as objects, this is because TypeScript only supports serialization of base types, here are the rules for serialization of base types.

  • Numeric serialized as numeric
  • string serialized as String
  • boolean serialized as Boolean
  • any serialized as Object
  • void serializes as undefined
  • Array serialized as Array
  • if Tuple serialized as Array
  • If an Class serializes as a constructor for a class
  • If an Enum serialized it as Number
  • If an Enum serialized as Number
  • If it has at least one call signature, then serialize it as Function
  • otherwise serialized as object Object, including interfaces

Interfaces and literal objects may also use complex type serialization in the future, but it is not available now.

Serialization of complex types

The TypeScript team is working on a proposal that will allow us to generate metadata for complex types.

Their proposal describes how some complex types will be serialized. The serialization rules above will still be used for basic types, but complex types will use a different serialization logic. In the proposal, there is a basic type used to describe all possible types.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/** 
  * Basic shape for a type.
  */
interface _Type {
  /** 
    * Describes the specific shape of the type.
    * @remarks 
    * One of: "typeparameter", "typereference", "interface", "tuple", "union", 
    * or "function".
    */
  kind: string; 
}

We can also find classes that are used to describe each of the possible types. For example, we can find the class foo <bar> {/ * ... * /}.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
  * Describes a generic interface.
  */
interface InterfaceType extends _Type {
  kind: string; // "interface"

  /**
    * Generic type parameters for the type. May be undefined.
    */
  typeParameters?: TypeParameter[];

  /**
    * Implemented interfaces.
    */
  implements?: Type[];

  /**
    * Members for the type. May be undefined. 
    * @remarks Contains property, accessor, and method declarations.
    */
  members?: { [key: string | symbol | number]: Type; };

  /**
    * Call signatures for the type. May be undefined.
    */
  call?: Signature[];

  /**
    * Construct signatures for the type. May be undefined.
    */
  construct?: Signature[];

  /**
    * Index signatures for the type. May be undefined.
    */
  index?: Signature[];
}

As we saw above, there will be an attribute indicating the implemented interface.

1
2
3
4
/**
* Implemented interfaces.
*/
implements?: Type[];

This information can be used to perform certain operations, such as verifying that entities implement certain interfaces at runtime, which could be really useful for IoC containers.

We don’t know when complex type serialization support will be added to TypeScript, but we can’t wait, as we plan to use it to add some cool features to our excellent IoC container for JavaScript, InversifyJS.