Babel Plugin: TypeScript Decorator Metadata
This Babel plugin enables the emission of design-time type metadata for TypeScript decorators when transpiling with Babel. It replicates the functionality provided by the TypeScript compiler's `emitDecoratorMetadata` flag, which is essential for frameworks and libraries like NestJS and TypeORM that rely on runtime type reflection for features such as Dependency Injection. The current stable version is `0.4.0`, released in October 2025, which introduced support for Babel 8. The project has a relatively slow release cadence, indicating a maintenance-driven development. It differentiates itself by bridging a crucial gap in Babel's TypeScript preset, allowing developers to leverage advanced decorator patterns without needing the TypeScript compiler for compilation.
Common errors
-
Error: Decorators are not valid here. This error usually occurs when the decorator plugin is not configured correctly or is placed in the wrong order.
cause The `babel-plugin-transform-typescript-metadata` is positioned incorrectly (after `@babel/plugin-proposal-decorators`) or `@babel/plugin-proposal-decorators` is missing `{'legacy': true}`.fixIn your Babel config, ensure `babel-plugin-transform-typescript-metadata` is the first plugin listed, and `@babel/plugin-proposal-decorators` is configured as `['@babel/plugin-proposal-decorators', { 'legacy': true }]`. -
TypeError: Reflect.getMetadata is not a function
cause The `reflect-metadata` polyfill, which provides the `Reflect.getMetadata` function, has not been loaded at runtime.fixInstall `reflect-metadata` (`npm install reflect-metadata`) and add `import 'reflect-metadata';` as the very first line in your application's main entry file (e.g., `src/main.ts`). -
TypeError: Cannot read properties of undefined (reading 'constructor') or similar runtime errors when using decorated classes/properties.
cause This often indicates that the design-time metadata for a decorated class or property is missing or incorrect, typically because `reflect-metadata` was not loaded, or the Babel plugin failed to emit the metadata due to misconfiguration.fixVerify that `reflect-metadata` is imported at the application's entry point and that the Babel plugin (`babel-plugin-transform-typescript-metadata`) is correctly configured (especially plugin order and `legacy: true` for decorators).
Warnings
- breaking The `babel-plugin-transform-typescript-metadata` plugin MUST be placed BEFORE `@babel/plugin-proposal-decorators` in your Babel configuration's `plugins` array. Incorrect ordering will result in the decorator metadata not being emitted, leading to runtime errors in libraries that depend on it.
- gotcha The `reflect-metadata` polyfill is a runtime requirement for applications consuming the metadata emitted by this plugin. It is not a direct dependency of the plugin and must be installed separately (`npm install reflect-metadata`) and imported once, typically at the entry point of your application (e.g., `import 'reflect-metadata';`).
- breaking To ensure proper parsing and transformation of decorators when using this plugin, `@babel/plugin-proposal-decorators` must be configured with the `{'legacy': true}` option. Failing to do so can lead to syntax errors or incorrect decorator behavior.
- breaking Version `0.4.0` introduced compatibility with Babel 8. If your project uses Babel 7, you should ensure your `@babel/core` peer dependency is compatible and consider using an earlier version of this plugin (e.g., `0.3.x`) to avoid potential compatibility issues.
- gotcha When implementing property injection with certain dependency injection libraries (like InversifyJS), Babel's decorator transformation might behave differently than TypeScript's native compilation. This can lead to runtime errors requiring specific helper functions (e.g., `fixPropertyDecorator` documented in the README) to ensure compatibility.
Install
-
npm install babel-plugin-transform-typescript-metadata -
yarn add babel-plugin-transform-typescript-metadata -
pnpm add babel-plugin-transform-typescript-metadata
Imports
- babel-plugin-transform-typescript-metadata
import plugin from 'babel-plugin-transform-typescript-metadata'; // This is a Babel plugin, not intended for direct import in application code. const plugin = require('babel-plugin-transform-typescript-metadata'); // Incorrect usage in application code; it's configured by Babel.// In your Babel configuration file (e.g., .babelrc.js or babel.config.js): module.exports = { plugins: [ "babel-plugin-transform-typescript-metadata", // ... other plugins, ensuring this one is first ], };
Quickstart
import 'reflect-metadata'; // Essential for runtime metadata access
import { injectable, inject } from 'inversify'; // Example: InversifyJS for DI
// Define an interface for clarity
interface Logger {
log(message: string): void;
}
// Implement the interface and make it injectable
@injectable()
class ConsoleLogger implements Logger {
private readonly prefix: string;
constructor(@inject('logPrefix') prefix: string) {
this.prefix = prefix;
}
log(message: string): void {
console.log(`[${this.prefix}] ${message}`);
}
}
// A service that depends on a Logger
@injectable()
class DataService {
@inject('Logger') // Property injection example
private readonly logger!: Logger;
private readonly apiUrl: string;
constructor(@inject('apiUrl') apiUrl: string) { // Constructor injection example
this.apiUrl = apiUrl;
}
fetchData(): void {
this.logger.log(`Fetching data from ${this.apiUrl}...`);
// Simulate fetching data
const data = { id: 1, name: 'Sample Data' };
this.logger.log(`Data received: ${JSON.stringify(data)}`);
// Demonstrate emitted metadata (runtime reflection)
console.log('\n--- Runtime Metadata for DataService.logger ---');
const loggerType = Reflect.getMetadata('design:type', DataService.prototype, 'logger');
console.log('design:type (logger):', loggerType ? loggerType.name : 'unknown');
console.log('\n--- Runtime Metadata for ConsoleLogger constructor ---');
const constructorParams = Reflect.getMetadata('design:paramtypes', ConsoleLogger);
console.log('design:paramtypes (ConsoleLogger):', constructorParams ? constructorParams.map((p: any) => p.name) : 'unknown');
}
}
// To run this, you would typically use a Babel setup:
// 1. Install dependencies: `npm install --save-dev @babel/core @babel/preset-typescript @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties`
// `npm install reflect-metadata inversify`
// 2. Configure Babel (e.g., in babel.config.js):
// module.exports = {
// plugins: [
// 'babel-plugin-transform-typescript-metadata',
// ['@babel/plugin-proposal-decorators', { 'legacy': true }],
// ['@babel/plugin-proposal-class-properties', { 'loose': true }],
// ],
// presets: [
// '@babel/preset-typescript',
// ],
// };
// --- Manual setup to run the example without a full Inversify container ---
// (In a real app, an Inversify container would handle instantiation and injection)
// Mock 'inject' for compile-time type checking and demonstration
class FakeInject { constructor(id: string) { return (target: any, key: string, index?: number) => {}; } }
const fakeInject = (id: string) => new FakeInject(id) as any;
// Manually create instances and inject for demonstration
const logPrefix = 'APP';
const consoleLogger: Logger = new ConsoleLogger(logPrefix);
const apiUrl = 'https://api.example.com/data';
const dataService = new DataService(apiUrl);
// Simulate property injection
(dataService as any).logger = consoleLogger;
dataService.fetchData();