esbuild doesn’t support TypeScript decorators out of the box because they are an experimental feature in TypeScript, and esbuild’s primary goal is speed, which often means sticking to stable language features.
Let’s get decorators working in your esbuild build.
First, you need to tell TypeScript that you want to use decorators. This is done in your tsconfig.json file. Add experimentalDecorators: true to your compilerOptions.
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"experimentalDecorators": true,
"emitDecoratorMetadata": true, // Often used with experimentalDecorators
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"]
}
The emitDecoratorMetadata option is also crucial if your decorators rely on type information at runtime (common in frameworks like NestJS or TypeORM).
Now, esbuild needs to be told how to handle these decorators. Since esbuild doesn’t transpile decorators itself, you’ll use a plugin to hook into esbuild’s build process and delegate the decorator transformation to a tool that does understand them, like @babel/plugin-proposal-decorators.
Install the necessary Babel packages:
npm install --save-dev @babel/core @babel/plugin-proposal-decorators @babel/plugin-syntax-decorators
Next, configure esbuild to use a Babel plugin. You’ll typically do this via esbuild’s plugins array in your build script or configuration file.
Here’s a simplified example of an esbuild build script using Node.js:
// build.js
const esbuild = require('esbuild');
const babelPlugin = require('@babel/core');
async function build() {
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
platform: 'node', // or 'browser'
format: 'cjs', // or 'esm'
plugins: [
{
name: 'babel-plugin-transform-decorators',
setup(build) {
build.onLoad({ filter: /\.ts$/ }, async (args) => {
const code = require('fs').readFileSync(args.path, 'utf8');
const result = await babelPlugin.transformAsync(code, {
filename: args.path,
presets: [
['@babel/preset-typescript', { allExtensions: true, isTSX: true }]
],
plugins: [
['@babel/plugin-proposal-decorators', { legacy: false }], // Use legacy: false for modern decorators
'@babel/plugin-syntax-decorators'
],
sourceMaps: true,
sourcemap: true,
ast: true,
});
return {
contents: result.code,
loader: 'ts',
sourcefile: args.path,
sourcemap: result.map,
};
});
},
},
],
});
console.log('Build complete!');
}
build().catch((e) => console.error(e));
In this setup, the babel-plugin-transform-decorators plugin intercepts .ts files. It reads the source code, then uses @babel/core to transform it with specific Babel plugins for decorators and TypeScript presets. The transformed JavaScript code (and its source map) is then returned to esbuild for its own bundling and minification steps.
Crucially, ensure your Babel configuration matches your TypeScript decorator stage. If you’re using the newer decorator syntax (stage 3 proposal), set legacy: false in @babel/plugin-proposal-decorators. If you’re on an older project using the legacy syntax, set legacy: true. The legacy: false option is generally recommended for new projects.
The @babel/plugin-syntax-decorators is included to ensure Babel can correctly parse the decorator syntax, even if the transformation itself is handled by @babel/plugin-proposal-decorators.
After running your build script (e.g., node build.js), you should find your code in the dist directory, correctly transpiled with decorators handled.
The next error you’ll encounter is likely a ReferenceError: __decorate is not defined if emitDecoratorMetadata is enabled but the necessary runtime helpers are not being included or are being stripped by esbuild in a way that breaks the decorator metadata.