目次を表示する

NestJS Deep Dive 2026 ─ 内部構造・再発明回避・パフォーマンスを 10 章で読み解く

Decorator と Reflect.metadata ─ 二層構造を読み解く

第2章: Decorator と Reflect.metadata ─ 二層構造を読み解く

Decorator 二層構造と design:paramtypes

第1章で DI コンテナが Reflect.getMetadata() を呼んで Provider の依存を解決すると書いた。その前に誰がメタデータを書き込んだのか? 答えは Decorator だ。本章では NestJS の Decorator が「メタデータ宣言実行時 hook」の二層で動く仕組みを分解する。

これを理解すると:

  • なぜ @Injectable() を付けるだけで DI が効くのか
  • Custom Decorator を自分で書ける
  • TypeScript の Stage 3 Decorator になぜ NestJS が乗らないのか
  • DiscoveryService で「特定 Decorator が付いた全 method を集める」ような自前フレームワーク的拡張ができる

Decorator とは「メタデータ宣言 + 実行時 hook」

NestJS の Decorator は、機能的に2層で動く。

graph TB
    Dec[Decorator]
    Dec --> Meta[層 1: メタデータ宣言<br/>Reflect.defineMetadata]
    Dec --> Hook[層 2: 実行時 hook<br/>関数を返してパラメータ加工等]
    Meta --> Read[NestJS が起動時に Reflect.getMetadata で読む]
    Hook --> Run[リクエストごとに実行される]
    style Meta fill:#1a2030,stroke:#4cc9f0
    style Hook fill:#1a2030,stroke:#ff4d6d

たとえば:

  • @Module(metadata)層1のみ:メタデータを class に貼るだけ
  • @Get('/users')層1のみ:HTTP メソッドとパスをメソッドに貼るだけ
  • @Body()層2あり:リクエストから body を取り出すロジックを実行

「Decorator = 関数」と覚えると、何が起きているかが見えやすい。

層1:メタデータの格納場所

Reflect.defineMetadata(key, value, target) は、内部的には target オブジェクトに直接プロパティを生やしているreflect-metadata ポリフィルの実装は target[Symbol(metadataKey)] のような形でメタデータマップを保持する。

@Injectable()
export class CatsService {
  constructor(private readonly repo: CatsRepository) {}
}

これがコンパイルされると、概念的には:

let CatsService = class CatsService {
  constructor(repo) { this.repo = repo; }
};
CatsService = __decorate([
  Injectable(),
  __metadata("design:paramtypes", [CatsRepository])
  //              ↑ ここが鍵:TypeScript が自動生成する型情報
], CatsService);

__metadata("design:paramtypes", [CatsRepository])コンパイル時に自動生成される。これが「TypeScript の emitDecoratorMetadata を有効にするとコンストラクタの型情報が runtime に出る」の正体。

NestJS の DI コンテナは:

const types = Reflect.getMetadata('design:paramtypes', CatsService);
// → [CatsRepository]

を読んで「CatsService のコンストラクタには CatsRepository を渡せばいい」と理解する。@Injectable() の本質的な役割は、TypeScript に design:paramtypes を出させることだ。

tsconfig.json の必須項目

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

この 2 つが揃っていないと Reflect.getMetadata('design:paramtypes', target)undefined を返し、DI が壊れる。

TypeScript Stage 3 Decorator となぜ乗らないか

TypeScript 5.0(2023-03)で TC39 Stage 3 Decorator がサポート開始された。Stage 3 は ECMAScript 標準化を視野に入れた仕様で、context.metadata という形でメタデータを直接書き込めるようになった。

理論上は reflect-metadata 不要」と言われるが、NestJS は依然として experimentalDecorators + emitDecoratorMetadata(Stage 2 互換) を使っている。理由は明確だ。

Stage 2(現行 NestJS)Stage 3(TS 5.x で標準化進行中)
__decorate + __metadata でコンパイル時に型情報を出すコンストラクタ引数の型情報を runtime に出す標準仕様がない
Reflect.metadata ポリフィルが業界標準context.metadata は class / method 単位のみ
@nestjs/typeorm@nestjs/mongooseclass-validator など周辺パッケージが Stage 2 前提エコシステム移行に時間がかかる

要するに 「コンストラクタ引数の型情報を実行時に得る」標準的な手段が Stage 3 にはまだない。これがある限り、NestJS は Stage 2 を使い続ける可能性が高い。

ただし、v12 ロードマップでは ESM / Vitest / Rspack 移行とともに Standard Schema 対応(Zod / Valibot / ArkType を @Body() で直接使える)が予定されている。Decorator 仕様の動向は引き続き注目。

主要 Decorator の実体

NestJS の主要 Decorator が「何をしているか」を内部的に見てみる(簡略版):

// @Module(metadata) の概念実装
function Module(metadata: ModuleMetadata): ClassDecorator {
  return (target: Function) => {
    Reflect.defineMetadata(MODULE_METADATA.IMPORTS, metadata.imports || [], target);
    Reflect.defineMetadata(MODULE_METADATA.PROVIDERS, metadata.providers || [], target);
    Reflect.defineMetadata(MODULE_METADATA.CONTROLLERS, metadata.controllers || [], target);
    Reflect.defineMetadata(MODULE_METADATA.EXPORTS, metadata.exports || [], target);
  };
}

// @Controller(prefix) の概念実装
function Controller(prefix: string): ClassDecorator {
  return (target: Function) => {
    Reflect.defineMetadata(PATH_METADATA, prefix, target);
  };
}

// @Injectable() の概念実装
function Injectable(options?: InjectableOptions): ClassDecorator {
  return (target: Function) => {
    Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
    if (options?.scope) {
      Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
    }
    // ↑ ここに加えて、TypeScript が __metadata("design:paramtypes", [...]) を出す
  };
}

NestJS が起動時に Reflect.getMetadata() で読み取り、Module ツリーを構築する。

Custom Decorator を作る

メタデータを「読む側」を理解したら、「書く側」も書けるようになる。NestJS には主に 3 つの Custom Decorator パターンがある。

1. SetMetadata:シンプルなメタデータ付与

import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// 使う側
@Roles('admin')
@Get()
findAll() {}

Reflect.getMetadata('roles', handler) で取り出せる。

2. Reflector.createDecorator<T>:型付き Decorator(v9 以降)

import { Reflector } from '@nestjs/core';

export const Roles = Reflector.createDecorator<string[]>();

// 使う側
@Roles(['admin'])
@Get()
findAll() {}

// 読む側
const roles = this.reflector.get(Roles, context.getHandler());
// ↑ roles は string[] と推論される(型安全)

文字列キーで管理する必要がなく、TypeScript の型推論が効く。NestJS v9 以降の推奨方式。

3. createParamDecorator:パラメータ Decorator

@Body()@Param() のような、リクエストから値を取り出す Decorator。

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: string | undefined, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return data ? request.user?.[data] : request.user;
  },
);

// 使う側
@Get()
profile(@User() user: UserEntity) {} // user オブジェクト全部
@Get('email')
email(@User('email') email: string) {} // email だけ

これが「層 2:実行時 hook」の典型。ファクトリ関数がリクエストごとに実行されて値を返す。

Reflector ─ 読み取りの API

メタデータを読む API は Reflector クラスにまとまっている。

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // 1. 単一メタデータ取得
    const roles = this.reflector.get(Roles, context.getHandler());

    // 2. 複数ターゲットからマージ(v11 でオブジェクト直返し)
    const merged = this.reflector.getAllAndMerge(Roles, [
      context.getHandler(),
      context.getClass(),
    ]);

    // 3. handler 優先で override
    const overridden = this.reflector.getAllAndOverride(Roles, [
      context.getHandler(),
      context.getClass(),
    ]);

    return /* 判定ロジック */;
  }
}

v11 の変更点

  • getAllAndMerge が単一要素配列ではなくオブジェクトを直接返す
  • getAllAndOverride の戻り値型が T | undefined

DiscoveryService と MetadataScanner ─ 自前フレームワーク的拡張

ここまで来ると「特定 Decorator が付いた全 method を起動時に集める」ような自前フレームワーク的な拡張が書けるようになる。

import { DiscoveryService, MetadataScanner } from '@nestjs/core';

@Injectable()
export class EventListenerLoader implements OnModuleInit {
  constructor(
    private discovery: DiscoveryService,
    private scanner: MetadataScanner,
  ) {}

  onModuleInit() {
    const providers = this.discovery.getProviders();

    for (const wrapper of providers) {
      const { instance } = wrapper;
      if (!instance) continue;

      const prototype = Object.getPrototypeOf(instance);
      this.scanner.getAllMethodNames(prototype).forEach(method => {
        const eventName = Reflect.getMetadata('event_listener', prototype[method]);
        if (eventName) {
          // この method が @EventListener(eventName) を持つ
          this.register(eventName, instance, method);
        }
      });
    }
  }
}

これが NestJS の EventEmitterModule@OnEvent)、ScheduleModule@Cron)、@nestjs/cqrs@CommandHandler)の内部で実際に行われていることだ。

つまり、@OnEvent のような「Decorator + 起動時収集」パターンは自分でも書ける。社内独自の Annotation 駆動 Framework を作るときの基盤になる。

メタデータのメモリコスト

各 class / method / property に Map が生えるため、大規模アプリ(数千 provider)では起動時にメタデータの shallow copy で MB 単位のメモリを消費する。

NestJS v11 で Module 解決アルゴリズムがオブジェクト参照ベースに変更されたのは、これを軽減して startup を高速化する目的のひとつ。第4章で扱う。

✅ 良い使い方 / ❌ 悪い使い方

// ❌ 悪い:文字列キーが散らばる
SetMetadata('isPublic', true);
const isPublic = reflector.get<boolean>('isPublic', handler);

// ✅ 良い:Reflector.createDecorator で型安全
export const IsPublic = Reflector.createDecorator<boolean>();
const isPublic = this.reflector.get(IsPublic, handler);

// ❌ 悪い:Custom ParamDecorator で重い処理
export const ExpensiveUser = createParamDecorator(
  async (data, ctx) => {
    return await db.users.findOne(/* ... */); // ← リクエストごとに DB 引く
  },
);

// ✅ 良い:パラメータ Decorator は純粋に context から値を取るだけ
export const User = createParamDecorator(
  (data, ctx) => ctx.switchToHttp().getRequest().user,
);
// 重い処理は Guard / Interceptor / Service に

本章の要点

#要点
1NestJS の Decorator は 「メタデータ宣言 + 実行時 hook」の二層
2@Injectable() の本質は「TypeScript に design:paramtypes を出させること」
3experimentalDecorators + emitDecoratorMetadata両方が必須(片方だけだと DI が壊れる)
4Stage 3 Decorator にまだ乗らない理由:コンストラクタ引数の型情報を runtime に出す標準仕様がない
5Custom Decorator のパターン:SetMetadata / Reflector.createDecorator<T>(v9+ 推奨) / createParamDecorator
6v11 で Reflector.getAllAndMerge がオブジェクト直返しに変更
7DiscoveryService + MetadataScanner「特定 Decorator が付いた全 method を集める」自前フレームワークが書ける
8パラメータ Decorator では重い処理を書かない。コンテキストから値を取るだけにする

効いている根本原理

本章は 原理2(Decorator は「メタデータ宣言 + 実行時 hook」の二層) を分解した章だった。@Injectable() がなぜ動くのか、Custom Decorator をどう作るかが分かれば、@OnEvent / @Cron / @CommandHandler がやっていることも自分の手で再現できる。

第1章の DI コンテナと本章の Decorator が起動時の話だったのに対し、次章は実行時の話 ─ リクエストが来てからレスポンスを返すまでの 6 つの層を順序通りに分解する。