Home

Awesome

typescript-rtti

Version CircleCI

Try it now | NPM | Github

Status: This software is release candidate quality

A Typescript transformer to implement comprehensive runtime type information (RTTI).

Projects using typescript-rtti

Send a pull request to feature your project!

Examples

Classes

class User {
    id : number;
    username? : string;
    protected favoriteColor? : number | string;
    doIt() { return 123; }
}

reflect(User)
    .getProperty('favoriteColor')
    .type.is('union');
    // => true

reflect(User)
    .getMethod('doIt')
    .type.isClass(Number);
    // => true

Interfaces

interface User {
    id : number;
    username? : string;
    protected favoriteColor? : number | string;
    doIt() { return 123; }
}

reflect<User>().as('interface')
    .reflectedInterface
    .getProperty('username')
    .isOptional
    // => true

Functions

function foo(id : number, username : string, protected favoriteColor? : number | string) {
    return id;
}

reflect(foo)
    .getParameter('username')
    .type.isClass(String)
    // => true

reflect(foo)
    .getParameter('favoriteColor')
    .type.is('union')
    // => true

Call sites

import { CallSite } from 'typescript-rtti';
function foo<T>(num : number, callSite? : CallSite) {
    reflect(callSite)
        .typeParameters[0]
        .isClass(Boolean)
        // => true

    reflect(callSite)
        .parameters[0]
        .isClass(Number)
        // => true

    reflect(callSite)
        .parameters[0]
        .is('literal')
        // => true

    reflect(callSite)
        .parameters[0].as('literal')
        .value
        // => 123
}

// The call-site type information is automatically serialized

foo<Boolean>(123);

More examples:

import { reflect } from 'typescript-rtti';

class A {
    constructor(
        readonly someValue : Number,
        private someOtherValue : string
    ) {
    }
}

class B {
    private foo : A;
    bar = 123;
    baz(): A {
        return this.foo;
    }
}

let aClass = ;
reflect(A).parameterNames                        // => ["someValue", "someOtherValue"]
reflect(A).parameters[0].name                    // => "someValue"
reflect(A).getParameter('someValue').type        // => Number
reflect(A).getParameter('someOtherValue').type   // => String

let bClass = reflect(B);
reflect(B).propertyNames                         // => ["foo", "bar"]
reflect(B).getProperty('foo').type               // => A
reflect(B).getProperty('foo').visibility         // => "private"
reflect(B).getProperty('bar').type               // => Number
reflect(B).methodNames                           // => [baz]
reflect(B).getMethod('baz').returnType           // => A

// ...These are just a few of the facts you can introspect at runtime

Setup

Prerequisites

Installation

npm install typescript-rtti reflect-metadata
npm install ttypescript -D

Setting up tsconfig.json

// tsconfig.json
"compilerOptions": {
    "plugins": [{ "transform": "typescript-rtti/dist/transformer" }]
}

In order for the transformer to run during your build process, you must use ttsc instead of tsc (or use one of the case specific solutions below).

// package.json
{
    "scripts": {
        "build": "ttsc -b"
    }
}

The type information is emitted using reflect-metadata. You'll need to import it as early in your application as possible and ensure that it is imported only once.

import "reflect-metadata";

ts-node

You can also use ts-node, just pass -C ttypescript to make sure ts-node uses typescript compiler which respects compiler transforms.

Webpack

See awesome-typescript-loader

Parcel

Unfortunately, because Parcel processes each Typescript file individually, it is currently not possible to use RTTI for projects that are transpiled directly by Parcel, even if you use a Parcel transformer that supports Typescript transformers. We are actively investigating improvements to this situation, but it will likely require writing a new Parcel transformer which builds your Typescript files as a complete compilation unit, and it is not yet clear if that is feasible given the design decisions that Parcel has made.

In the mean time, you may be able to work around this by first compiling using ttsc and then feeding the result into Parcel.

Rollup

Unlike Parcel, Rollup builds your Typescript files as a combined unit, so type checking, cross-file features, and typescript-rtti work just fine when using the official Typescript plugin. Using the sucrase plugin is not supported.

Jest

See https://github.com/rezonant/typescript-rtti-jest for a sample repo with jest setup.

Features

Using reflect API

The reflect() API is the entry point to all types of reflection offered by typescript-rtti.

Reflecting on Interfaces

reflect<Type>() returns a type reference, which could be a union type, intersection type, a class, a literal type, the null type, or indeed, an interface type. You can narrow the returned ReflectedTypeRef using is() or as(). The is() function is a type guard, and as() returns the value casted to the appropriate type as well as performing a runtime assertion that the cast is correct.

If you want to reflect upon the properties and methods of an interface, you'll want to obtain the reflectedInterface:

interface Foo {
    foo : string;
}

let reflectedInterface = reflect<Foo>().as('interface').reflectedInterface;

expect(reflectedInterface.getProperty('foo').type.isClass(String))
    .to.be.true;

You can encapsulate this away from users of your library using call site reflection.

Call site Reflection

Many users are interested in passing type information in the form of a generic parameter. This is supported via "call site reflection". The "call site" is the function call which executed the current invocation of a function. Call site reflection allows you to reflect on both the generic and parameter types of a given function call as opposed to those defined on a function declaration.

This type of reflection carries a cost, not only of the serialization of the types themselves but also for the performance of the function that accepts the call-site information. It may cause the Javascript engine to mark such a function as megamorphic and thus ineligible for optimization. It is therefore important to ensure that functions which wish to receive call-site information must opt in so that function calls in general retain the same performance characteristics as when the transformer isn't used to compile a codebase.

Accepting a function parameter marked with the CallSite type is how typescript-rtti knows that call site information should be passed to the function. When making calls from one call-site enabled function to another, typescript-rtti automatically passes generic types along. Thus the following example works as expected:

function foo<T>(call? : CallSite) {
    expect(reflect(call).typeParameters[0].isClass(String)).to.be.true;
}

function bar<T>(call? : CallSite) {
    foo<T>();
}

bar<String>();

Checking the type of a value at runtime

A common use case of runtime type information is to validate that a value matches a specific Typescript type at runtime. This functionality is built in via the matchesValue() API:

interface A {
    foo: string;
    bar: number;
    baz?: string;
}

reflect<A>().matchesValue({ foo: 'hello' }) // false
reflect<A>().matchesValue({ foo: 'hello', bar: 123 }) // true
reflect<A>().matchesValue({ foo: 'hello', bar: 123, baz: 'world' }) // true
reflect<A>().matchesValue({ foo: 123, bar: 'hello' }) // false
reflect<A>().matchesValue({ }) // false

This works for all types that typescript-rtti can reflect, including unions, intersections, interfaces, classes, object literals, intrinsics (true/false/null/undefined), literals (string/number) etc.

Regarding design:*

This library supports emitDecoratorMetadata but does not require it.

When you use this transformer, Typescript's own emitting of the design:* metadata is automatically disabled so that this transformer can handle it instead. Note that there are limitations with this metadata format (it has problems with forward references for one) and if/when the Typescript team decides to further advance runtime metadata, it is likely to be changed.

Enabling emitDecoratorMetadata causes typescript-rtti to emit both the design:* style of metadata as well as its own rt:* format. Disabling it causes only rt:* metadata to be emitted.

Unsupported Scenarios

Some Typescript options are incompatible with typescript-rtti:

Skipping parts of your code

If, for whatever reason, you wish to skip generating RTTI metadata for a part of a source file, you can use the @rtti:skip JSDoc tag. The transformer will skip processing the node this tag is on, and any child nodes. This works for any syntactic element that Typescript supports JSDocs on (which is more than you might think).

Backward Compatibility

The library is in beta, so currently no backward compatibility is guaranteed but we are tracking back-compat breakage in CHANGELOG.md as we approach a release with proper adherence to semver.

We do not consider a change which causes the transformer to emit a more specific type where it used to emit Object as breaking backwards compatibility, but we do consider changes to other emitted types as breaking backward compatibility.

Format

The metadata emitted has a terse but intuitive structure. Note that you are not intended to access this metadata directly, instead you should use the built-in Reflection API (ReflectedClass et al).

Class Sample


//input

export class B {
    constructor(
        readonly a : A
    ) {
    }
}

// output

const __RΦ = {
    m: (k, v) => Reflect.metadata(k, v)
};
//...
B = __decorate([
    __RΦ.m("rt:P", ["a"]),
    __RΦ.m("rt:p", [{ n: "a", t: () => A, f: "R" }]),
    __RΦ.m("rt:f", "C$")
], B);

Method Sample


//input

export class A {
    takeShape(shape? : Shape): Shape {
        return null;
    }

    haveAnArray(myArray : string[]) {
        return 123;
    }

    naturalTypes(blank, aString : string, aNumber : number, aBool : boolean, aFunc : Function) {
        return 'hello';
    }
}

// output

//...
__decorate([
    __RΦ.m("rt:p", [{ n: "shape", t: () => ShapeΦ, f: "?" }]),
    __RΦ.m("rt:f", "M$"),
    __RΦ.m("rt:t", () => ShapeΦ)
], A.prototype, "takeShape", null);
__decorate([
    __RΦ.m("rt:p", [{ n: "myArray", t: () => [String] }]),
    __RΦ.m("rt:f", "M$"),
    __RΦ.m("rt:t", () => Number)
], A.prototype, "haveAnArray", null);
__decorate([
    __RΦ.m("rt:p", [{ n: "blank", t: () => void 0 }, { n: "aString", t: () => String }, { n: "aNumber", t: () => Number }, { n: "aBool", t: () => Boolean }, { n: "aFunc", t: () => Function }]),
    __RΦ.m("rt:f", "M$"),
    __RΦ.m("rt:t", () => String)
], A.prototype, "naturalTypes", null);
A = __decorate([
    __RΦ.m("rt:m", ["takeShape", "haveAnArray", "naturalTypes"]),
    __RΦ.m("rt:f", "C$")
], A);

Why the symbols / Why not use Symbols?

The phi symbol is used on generated identifiers to prevent collisions and add a bit of difficulty for users trying to use the metadata directly. Due to the way metadata generation works it cannot be done using private Symbols, but the metadata generated should (to the end developer) be considered private.

Troubleshooting / FAQ

Q: Looks like it doesn't emit number | null as expected, I'm getting Number!

Typescript's strictNullChecks setting is the cause. When you have it disabled (default), number | null automatically collapses to number. When you have it enabled, number | null is emitted correctly when using typescript-rtti. We are investigating how to enable observing number | null without requiring strictNullChecks to be available, but the current behavior matches what Typescript sees.

Q: I receive RTTI: Failed to build source file: Cannot read properties of undefined (reading 'flags')

There are several potential causes of this and certainly one of those potential causes is that you've discovered a bug in the transformer. However, there are a few cases where this is known to occur in the current version:

Ideally typescript-rtti should fail gracefully under these conditions, but for now it will help avoid duplicate issue reports as the above are all already tracked in existing issues.

Related/Similar Projects