Run StackBlitz project.

RTTIST

Runtime Type Information
System for TypeScript

Runtime Reflection

Reflection provides objects describing types and modules. You can use reflection to look up, dynamically import and create an instance of a type, list its properties, accessors, methods, constructors, decorators and other information. You can also get the type of existing object and/or statically check or compare types. If you are using decorators in your code, reflection enables you to access them and read their constant arguments.

Type objects are inheritors of the Type class that is narrowable by the set of type-guards.

import { getType, Type, PropertyInfo, MethodInfo } from "rttist";

interface Employee {
	name: string;
	salary: number;
	sayHello();
	sayHello(toSomebody: string);
}

const type: Type = getType<Employee>();

if (type.isInterface()) {
	const properties = type.getProperties().map((prop: PropertyInfo) => prop.name);
	const methods = type.getMethods().map((method: MethodInfo) => method.name);
	
	console.log(properties); // > [ name, salary ]
	console.log(methods); // > [ sayHello ] 
}

Generic Type Parameters

You can get the real type of type parameter at runtime. Generic type parameter of type aliases, interfaces, functions, classes and methods are fully supported.

class Logger<TContext> {
	constructor() {
		this.context = getType<TContext>().name;
		
		console.log(
			"Gonna log under context", this.context
		);
	}

	changeContext<TNewContext>():  {
		this.context = getType<TNewContext>().name;

		console.log(
			"Changing context from",
			getType<TContext>().name,
			"to", this.context
		);
	}

	log(...args: any[]): void {
		console.log(`[${this.context}]`, ...args);
	}
}

const logger = new Logger<UserController>();
logger.log("Guest logged in."); // > [UserController] Guest logged in.
logger.changeContext<UserService>();
logger.log("New user created."); // > [UserService] New user created.

Static Type Checking

With Type objects you are able to validate types without value (or instance of object).

Every Type have method Type.is(type) to compare that two types are the same Type. Or for example the InterfaceType and the ClassType have method Type.isDerivedFrom() that checks if one type inherits from another.

interface ILoggable {
	log();
}

abstract class Person {}

export function logPerson<TPerson extends Person>(person: TPerson) {
	const personType: Type = getType<Person>();
	const personParamType: Type = getType<TPerson>();

	if (!personParamType.isSubclassOf(personType) 
		|| personParamType.is(personType) 
		|| personParamType.abstract
	) 
		throw new Error();
	
	if (personParamType.isDerivedFrom(getType<ILoggable>())) {
		person.log();
	}
	else {
		console.log(person);
	}
}

							

Searchable Metadata Library of Project Types and Modules

Static metadata library of all the types and modules generated from a project accessible via API at runtime.

Let's have an Entity interface from some ORM. We want to find all the not-abstract classes implementing that interface and do something with them. That is pretty simple task...

import { Metadata } from "rttist";
							
// First, get type of the interface `Entity`
const entityType = getType<Entity>();

// Get all the JS modules from current directory,
const allEntities: Type[] = Metadata.getModules()
	.filter(module => module.path.includes(__dirname))
	// select all the contained types,
	.flatMap(module => module.getTypes())
	// check if it is exported class,
	.filter(type => type.exported && type.isClass() 
		// not abstract and is derived from the `Entity`.
		&& !type.abstract && type.isDerivedFrom(entityType));

Vast Majority of Types Supported

Besides user defined functions, decorators, classes, interfaces and type aliases, reflection works with almost all native types. If there is a type we do not support, file an issue.

[
	any, unknown, undefined, null, void, string, String, number, Number, 
	boolean, Boolean, true, false, BigInt, Date, RegExp,
	Array<number>, Array<Array<number>>, number[], number[][], 
	Map<string, string>, WeakMap<Function, string>, 
	Set<string>, WeakSet<Function>, 
	Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
	Int32Array, Uint32Array, Float32Array, Float64Array,
	BigInt64Array, BigUint64Array,
	symbol, unique symbol, Symbol, Promise<boolean>, Error,
	ArrayBuffer, SharedArrayBuffer,
	Atomics, DataView, Generator,
	Iterable<any>, IterableIterator<any>,
	AsyncIterator<any>, AsyncGeneratorFunction, AsyncGenerator<any>,
]

The Power of Reflection

Reflection is a very poweful feature that allows you to interact with your objects and types at runtime. To get the type of variable that is unknown beforehand by the programmer, or to find and instantiate a user defined class in a framework. It is an interesting feature that allows you to write highly abstract code.

Imagine you are creating an MVC framework. You want to let your users declare classes (controllers) decorated by predefined set of decorators which will setup the controller's behavior.

How would you do that? How to even discover those controllers? Using file-system, preloading all the modules? Force user to register all the controllers manually? Well, there are many dirty ways, but it is clear and simple job for reflection.

// Let's have a controller decorated by route decorator
@route("users")
class UserController extends ControllerBase {
	get(id?: string) {
		// ...
	}
}
// Find all the controllers and register them in router
// Get the Type of the @route decorator.
const routeDecoratorType = getType<typeof route>();
							
// Get the Type of the ControllerBase class.
const baseType = getType<ControllerBase>();

// Find all classes extended from ControllerBase decorated by the @route.
const controllers = Metadata.getTypes()
	.filter(type => type.isClass() && type.isDerivedFrom(baseType))
	.map(type => ({
		type,
		routeDecorator: type.getDecorators()
			.find(decorator => decorator.is(routeDecoratorType))
	}))
	.filter(x => x.routeDecorator !== undefined);

controllers.map(async x => {
	const ClassCtor: typeof ControllerBase = await x.type.getCtor();
	
	// The @route decorator argument... "users" in case of UserController
	const route = x.routeDecorator.getArguments()[0];
	
	app.get("/" + route + "/:id", (req, res) => {
		return new ClassCtor().get(req.params.id);
	})
});

Works With 3rd Party Packages

Every package, using RTTIST or not, will work with your codebase. You can get type of any class, interface, type alias, function etc. imported from any package.

Packages using RTTIST can publish metadata along with source code which will reduce the time of your build in case you use types from those packages. If there is no metadata published, it will be generated.

// Using TypeORM package
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";

@Entity()
export class User extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    firstName: string

    @Column()
    lastName: string

    @Column()
    age: number
}
import { BaseEntity } from "typeorm";
import { Metadata } from "rttist";

const baseEntity = getType<BaseEntity>;

Metadata.getTypes()
	.filter(t => t.isClass() && t.isDerivedFrom(baseEntity));