Skip to main content
Version: Next (unreleased)

Decorators

There is no reflect-metadata at run time. Instead, the compiler builds a static description of each decorated class, a ClassInfo, and passes it to the decorators. Undecorated classes carry nothing.

Static reflection​

ClassInfo and its parts come from xisto:reflect:

InterfaceFields
ClassInfoid, name, params, construct(args), methods, fields, isInstance(v), base
MethodInfoname, params, returns, invoke(self, args)
FieldInfoname, optional, type, get(target)
TypeInfokind, id, decode(json), encode(value), schema (JSON Schema, OpenAPI 3.0 dialect)

A class decorator in xisto's form takes the ClassInfo:

import { ClassInfo } from "xisto:reflect";
const registry: ClassInfo[] = [];
function Entity(): (c: ClassInfo) => void {
return (c: ClassInfo) => { registry.push(c); };
}
@Entity()
class User { id: int = 0; name: string = ""; email: string | null = null; }

for (const c of registry) {
console.log(c.name, c.fields.map((f) => `${f.name}: ${f.type.kind}`).join(", "));
const u = c.construct([]) as User; // User id: int, name: string, email: string
console.log(c.isInstance(u)); // true
}

A method decorator in this form takes (c: ClassInfo, m: MethodInfo). This is how NestJS-style frameworks (routes, dependency injection, validation, Swagger schemas) are built on xisto: the information emitDecoratorMetadata would give at run time is generated at compile time.

Method decorators that replace the method​

TypeScript's form (target, key, descriptor) works, including decorators that replace descriptor.value:

function Timed(): MethodDecorator {
return (target: object, key: string | symbol, descriptor: PropertyDescriptor) => {
const original = descriptor.value as (...args: unknown[]) => unknown;
descriptor.value = function (this: unknown, ...args: unknown[]): unknown {
console.log(`-> ${String(key)}(${args.join(", ")})`);
return original.apply(this, args);
};
return descriptor;
};
}
class Calc {
base = 10;
@Timed() add(x: number): number { return this.base + x; }
twice(x: number): number { return this.add(x) * 2; } // goes through the wrapper
}

Only a decorated method gets a slot: calls to it, from outside and through this, go through the slot to the wrapper. Stacked decorators wrap bottom up. Every other method keeps its direct call, so code without such decorators compiles to the same machine code.

Prototype patching​

Libraries that instrument a driver patch a prototype. xisto supports that form: inside a function, Object.getPrototypeOf(x) where x comes from a method call on a parameter, then proto.m = function …:

function instrument(db: Db): void {
const sample = db.repo();
const proto = Object.getPrototypeOf(sample) as { find: (...args: unknown[]) => Promise<unknown> };
const original = proto.find;
proto.find = async function (this: unknown, ...args: unknown[]): Promise<unknown> {
console.log("find", args[0]);
return original.apply(this, args);
};
}

After the patch, every Repo.find call goes through the new function, as on Node. A patched method is called indirectly; avoid the pattern in hot code. Other prototype writes are refused (XS2017).

See also​