inversify/rflct

Ahead-of-time reflect metadata for TypeScript 7. Injects design:symbols, design:paramtypes, design:properties, and design:class at build time — no decorators, no emitDecoratorMetadata.

4

stars

10

commits

TypeScript

primary language

Aug 31, 2026

updated

dependency-injection
reflection
reflect-metadata
runtime-types
typescript

README

🪞 RFLCT: Runtime type metadata in TS 7.0+ No --emitDecoratorMetadataor decorators required

Ahead-of-time reflect metadata for TypeScript 7. Injects design:symbols, design:paramtypes, design:propertytype, design:properties, and design:class at build time — no decorators, no emitDecoratorMetadata.

Integrates with any build tool via unplugin (Vite, Rollup, webpack, esbuild), or use the CLI with the TypeScript 7 API for standalone tsgo projects.

Why does this exist? TC39 decorators are now standard, but emitDecoratorMetadata was never part of the spec and is being removed. RFLCT fills the gap by emitting type metadata from type annotations at compile time, using the same reflect-metadata runtime and design:* keys that the ecosystem already understands. Read the full Philosophy & Motivation.

Quick example

import { Reflect, resolve } from "rflct";

interface Shape { sides: number; }

class Polygon {
  public color: Reflect<string, { optional: true }>;

  constructor(
    public shape: Reflect<Shape>,
    public label: Reflect<string>
  ) {}
}

container.bind(resolve<Shape>()).to(Polygon);

Auto-reflect with Reflectable

When a class implements Reflectable, constructor parameters are reflected automatically — no Reflect<T> needed:

import { Reflectable, resolve } from "rflct";

interface Shape { sides: number; }

class Polygon implements Reflectable {
  constructor(
    public shape: Shape,
    public label: string
  ) {}
}

Both forms produce identical metadata. Use Reflect<T, M> when you need per-parameter metadata (optionality, names, tags), or Reflectable when every constructor parameter is injected with default metadata.

After transformation:

const __RFLCT_Shape = Symbol.for("@acme/shapes@1|src/geo.ts|Shape");

class Polygon {
  constructor(shape, label) {}
}

Reflect.defineMetadata("design:paramtypes", [
  { type: __RFLCT_Shape, metadata: {} },
  { type: String, metadata: {} }
], Polygon, undefined);

Reflect.defineMetadata("design:properties", ["color"], Polygon);
Reflect.defineMetadata("design:propertytype", [
  { type: String, metadata: { optional: true } }
], Polygon.prototype, "color");

container.bind(__RFLCT_Shape).to(Polygon);

Reflect.defineMetadata("design:symbols", Object.assign(
  Reflect.getMetadata("design:symbols", Reflect) ?? {}, {
    "@acme/shapes@1|src/geo.ts|Shape": __RFLCT_Shape,
    "@acme/shapes@1|src/geo.ts|Polygon": Polygon,
  }
), Reflect);

Documentation

GuideDescription
Philosophy & MotivationWhy RFLCT exists — TC39 decorators, emitDecoratorMetadata removal, and migration strategy
Constructor InjectionReflect<T> on constructor parameters — type mapping, optional metadata
Property InjectionReflect<T> on class properties — design:properties registry, per-property metadata
Method ParametersReflect<T> on method parameters — prototype-level metadata
Resolve Callsresolve<T>() — compile-time type resolution for DI bindings and map keys
Multi-InjectionReflect<T[]> — array types with elementType for injecting collections
Metadata ArgumentsReflect<T, M> — attaching arbitrary metadata (optionality, constraints, names)
Class MetadataReflectable<T> — class-level metadata via implements clauses, auto-reflect for constructor params
Type-Only SymbolsHow interfaces and type aliases get stable Symbol.for(...) runtime identities
Symbol QualificationThe package@major|path|Name format and cross-package interop guarantees
InternalsArchitecture — transform pipeline, CLI vs unplugin, type serialization, module structure

Four transformations

#Metadata keyWhat it does
1design:symbolsGlobal type registry — every class, interface, and type alias is registered process-wide
2design:paramtypesParameter type metadata — { type, metadata } entries for constructors and methods
2bdesign:propertytypeProperty type metadata — { type, metadata } entries for class properties
3design:propertiesProperty name registry — lists which properties on a class carry Reflect<T> annotations
4design:classClass-level metadata via Reflectable<T> in implements clauses

resolve<T>() is a fifth transformation that replaces calls with the runtime identity of T at compile time.

Installation

npm install rflct reflect-metadata

Import reflect-metadata once at your application entry point:

import "reflect-metadata";

This polyfills the global Reflect.defineMetadata / Reflect.getMetadata API that the generated code relies on. Do not import it in every file — a single import per process is sufficient.

Usage with build tools (unplugin)

The plugin runs with enforce: 'pre' and outputs JavaScript by default (types are stripped via oxc-transform), so it works regardless of what TypeScript transpiler the consumer has — or doesn't have. Pass { transpile: false } to output TypeScript instead and let the bundler's own TS plugin handle type stripping.

Vite

// vite.config.js
import { vitePlugin } from "rflct/vite";

export default {
  plugins: [vitePlugin()],
};

Rollup

// rollup.config.js
import { rollupPlugin } from "rflct/rollup";

export default {
  plugins: [rollupPlugin()],
};

webpack

// webpack.config.js
const { webpackPlugin } = require("rflct/webpack");

module.exports = {
  plugins: [webpackPlugin()],
};

esbuild

import { esbuildPlugin } from "rflct/esbuild";

await esbuild.build({
  plugins: [esbuildPlugin()],
});

Usage with TypeScript 7 CLI

For standalone tsgo projects without a bundler:

npx rflct -p tsconfig.json

Options:

  • -p, --project — path to tsconfig.json (default: tsconfig.json)
  • -h, --help — show help

The CLI type-checks original sources via the TypeScript 7 API (typescript/unstable/sync), then transforms and emits JavaScript. Output goes to the outDir specified in your tsconfig.

API

Types (imported by consumers)

// Marks a parameter/property for metadata injection. Erases to T.
type Reflect<T, Metadata = {}> = T;

// Phantom type for class-level metadata via implements clauses.
type Reflectable<T = {}> = { ... };

// Compile-time resolution — replaced by the transformer.
function resolve<T>(value?: abstract new (...args: any[]) => T): symbol | (abstract new (...args: any[]) => T);

Programmatic transform

import { transform } from "rflct/transform";

// Output TypeScript (metadata injected, types intact)
const result = transform(source, fileName);
// result.code        — transformed source
// result.transformed — whether any changes were made

// Output JavaScript (types stripped via oxc-transform)
const result = transform(source, fileName, { transpile: true });
// result.code — JavaScript output
// result.map  — source map (JSON string)

License

MIT

Contributors

remojansen

10 commits

inversify/rflct

Ahead-of-time reflect metadata for TypeScript 7. Injects design:symbols, design:paramtypes, design:properties, and design:class at build time — no decorators, no emitDecoratorMetadata.

4

stars

10

commits

TypeScript

primary language

Aug 31, 2026

updated

dependency-injection
reflection
reflect-metadata
runtime-types
typescript

README

🪞 RFLCT: Runtime type metadata in TS 7.0+ No --emitDecoratorMetadataor decorators required

Ahead-of-time reflect metadata for TypeScript 7. Injects design:symbols, design:paramtypes, design:propertytype, design:properties, and design:class at build time — no decorators, no emitDecoratorMetadata.

Integrates with any build tool via unplugin (Vite, Rollup, webpack, esbuild), or use the CLI with the TypeScript 7 API for standalone tsgo projects.

Why does this exist? TC39 decorators are now standard, but emitDecoratorMetadata was never part of the spec and is being removed. RFLCT fills the gap by emitting type metadata from type annotations at compile time, using the same reflect-metadata runtime and design:* keys that the ecosystem already understands. Read the full Philosophy & Motivation.

Quick example

import { Reflect, resolve } from "rflct";

interface Shape { sides: number; }

class Polygon {
  public color: Reflect<string, { optional: true }>;

  constructor(
    public shape: Reflect<Shape>,
    public label: Reflect<string>
  ) {}
}

container.bind(resolve<Shape>()).to(Polygon);

Auto-reflect with Reflectable

When a class implements Reflectable, constructor parameters are reflected automatically — no Reflect<T> needed:

import { Reflectable, resolve } from "rflct";

interface Shape { sides: number; }

class Polygon implements Reflectable {
  constructor(
    public shape: Shape,
    public label: string
  ) {}
}

Both forms produce identical metadata. Use Reflect<T, M> when you need per-parameter metadata (optionality, names, tags), or Reflectable when every constructor parameter is injected with default metadata.

After transformation:

const __RFLCT_Shape = Symbol.for("@acme/shapes@1|src/geo.ts|Shape");

class Polygon {
  constructor(shape, label) {}
}

Reflect.defineMetadata("design:paramtypes", [
  { type: __RFLCT_Shape, metadata: {} },
  { type: String, metadata: {} }
], Polygon, undefined);

Reflect.defineMetadata("design:properties", ["color"], Polygon);
Reflect.defineMetadata("design:propertytype", [
  { type: String, metadata: { optional: true } }
], Polygon.prototype, "color");

container.bind(__RFLCT_Shape).to(Polygon);

Reflect.defineMetadata("design:symbols", Object.assign(
  Reflect.getMetadata("design:symbols", Reflect) ?? {}, {
    "@acme/shapes@1|src/geo.ts|Shape": __RFLCT_Shape,
    "@acme/shapes@1|src/geo.ts|Polygon": Polygon,
  }
), Reflect);

Documentation

GuideDescription
Philosophy & MotivationWhy RFLCT exists — TC39 decorators, emitDecoratorMetadata removal, and migration strategy
Constructor InjectionReflect<T> on constructor parameters — type mapping, optional metadata
Property InjectionReflect<T> on class properties — design:properties registry, per-property metadata
Method ParametersReflect<T> on method parameters — prototype-level metadata
Resolve Callsresolve<T>() — compile-time type resolution for DI bindings and map keys
Multi-InjectionReflect<T[]> — array types with elementType for injecting collections
Metadata ArgumentsReflect<T, M> — attaching arbitrary metadata (optionality, constraints, names)
Class MetadataReflectable<T> — class-level metadata via implements clauses, auto-reflect for constructor params
Type-Only SymbolsHow interfaces and type aliases get stable Symbol.for(...) runtime identities
Symbol QualificationThe package@major|path|Name format and cross-package interop guarantees
InternalsArchitecture — transform pipeline, CLI vs unplugin, type serialization, module structure

Four transformations

#Metadata keyWhat it does
1design:symbolsGlobal type registry — every class, interface, and type alias is registered process-wide
2design:paramtypesParameter type metadata — { type, metadata } entries for constructors and methods
2bdesign:propertytypeProperty type metadata — { type, metadata } entries for class properties
3design:propertiesProperty name registry — lists which properties on a class carry Reflect<T> annotations
4design:classClass-level metadata via Reflectable<T> in implements clauses

resolve<T>() is a fifth transformation that replaces calls with the runtime identity of T at compile time.

Installation

npm install rflct reflect-metadata

Import reflect-metadata once at your application entry point:

import "reflect-metadata";

This polyfills the global Reflect.defineMetadata / Reflect.getMetadata API that the generated code relies on. Do not import it in every file — a single import per process is sufficient.

Usage with build tools (unplugin)

The plugin runs with enforce: 'pre' and outputs JavaScript by default (types are stripped via oxc-transform), so it works regardless of what TypeScript transpiler the consumer has — or doesn't have. Pass { transpile: false } to output TypeScript instead and let the bundler's own TS plugin handle type stripping.

Vite

// vite.config.js
import { vitePlugin } from "rflct/vite";

export default {
  plugins: [vitePlugin()],
};

Rollup

// rollup.config.js
import { rollupPlugin } from "rflct/rollup";

export default {
  plugins: [rollupPlugin()],
};

webpack

// webpack.config.js
const { webpackPlugin } = require("rflct/webpack");

module.exports = {
  plugins: [webpackPlugin()],
};

esbuild

import { esbuildPlugin } from "rflct/esbuild";

await esbuild.build({
  plugins: [esbuildPlugin()],
});

Usage with TypeScript 7 CLI

For standalone tsgo projects without a bundler:

npx rflct -p tsconfig.json

Options:

  • -p, --project — path to tsconfig.json (default: tsconfig.json)
  • -h, --help — show help

The CLI type-checks original sources via the TypeScript 7 API (typescript/unstable/sync), then transforms and emits JavaScript. Output goes to the outDir specified in your tsconfig.

API

Types (imported by consumers)

// Marks a parameter/property for metadata injection. Erases to T.
type Reflect<T, Metadata = {}> = T;

// Phantom type for class-level metadata via implements clauses.
type Reflectable<T = {}> = { ... };

// Compile-time resolution — replaced by the transformer.
function resolve<T>(value?: abstract new (...args: any[]) => T): symbol | (abstract new (...args: any[]) => T);

Programmatic transform

import { transform } from "rflct/transform";

// Output TypeScript (metadata injected, types intact)
const result = transform(source, fileName);
// result.code        — transformed source
// result.transformed — whether any changes were made

// Output JavaScript (types stripped via oxc-transform)
const result = transform(source, fileName, { transpile: true });
// result.code — JavaScript output
// result.map  — source map (JSON string)

License

MIT

Contributors

remojansen

10 commits

Languages

TypeScript

91.1%

JavaScript

8.9%