Problem

When running a TypeScript file with Bun from a different directory than where the tsconfig.json resides, Bun doesn’t pick up the TypeScript configuration. This causes emitDecoratorMetadata to not be applied, breaking NestJS dependency injection.

Symptoms

  • Constructor-injected services are undefined
  • Error: undefined is not an object (evaluating 'this.service.method')
  • NestJS modules initialize successfully but commands/controllers fail
  • Constructor is called before modules are initialized (visible in debug logs)

Reproduction

Monorepo Structure

project-root/
├── package.json # workspaces: ["packages/*"]
├── packages/
│ └── my-cli/
│ ├── package.json # name: "@project/my-cli"
│ ├── tsconfig.json # emitDecoratorMetadata: true
│ └── src/
│ └── main.ts

What Fails

Terminal window
# From project root - FAILS
bun run packages/my-cli/src/main.ts
# DI is broken, injected services are undefined

What Works

Terminal window
# From within package directory - WORKS
cd packages/my-cli
bun run src/main.ts
# Using workspace filter - WORKS
bun run --filter @project/my-cli dev
# Building first, then running built output - WORKS
cd packages/my-cli
bun build src/main.ts --outdir dist --target bun
bun run dist/main.js

Root Cause

Bun’s TypeScript transpiler looks for tsconfig.json relative to the current working directory, not relative to the file being executed. When running from project root:

  1. Bun looks for project-root/tsconfig.json
  2. Doesn’t find emitDecoratorMetadata: true (root tsconfig may not have it)
  3. Transpiles without decorator metadata
  4. NestJS DI can’t resolve constructor parameter types
  5. Services are injected as undefined

Solutions

In root package.json:

{
"scripts": {
"my-cli": "bun run --filter @project/my-cli dev"
}
}

In packages/my-cli/package.json:

{
"scripts": {
"dev": "bun run src/main.ts"
}
}

Usage:

Terminal window
bun run my-cli [args...]

Why it works: The --filter flag runs the script from within the package’s directory context, where Bun finds the correct tsconfig.json.

Solution 2: Build Before Running

Terminal window
# Build with Bun's bundler (preserves decorator metadata)
bun build src/main.ts --outdir dist --target bun --packages external
# Run built output
bun run dist/main.js

Why it works: The bundler properly processes TypeScript with the correct tsconfig, and the built JavaScript includes the decorator metadata.

Solution 3: Root tsconfig with Path Reference

Create project-root/tsconfig.json:

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

Note: This applies to ALL TypeScript files run from root, which may not be desirable.

Verification

Add debug logging to verify DI is working:

@Injectable()
export class MyCommand extends CommandRunner {
constructor(private readonly service: MyService) {
super();
console.log(`DEBUG: service injected = ${!!service}`);
}
}

Working output:

[Nest] ... [InstanceLoader] MyModule dependencies initialized
DEBUG: service injected = true

Broken output:

DEBUG: service injected = false
[Nest] ... [InstanceLoader] MyModule dependencies initialized

Note how in the broken case, the constructor runs before modules initialize.

Key Takeaway

Always run Bun TypeScript files from the directory containing the tsconfig.json, or use workspace filters to ensure correct configuration resolution.