Dev Study
← 解説「カスタムデコレータ — パラメータデコレータを自作する」に戻る

サンプルコードで身につける: カスタムデコレータ — パラメータデコレータを自作する

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1最小のパラメータデコレータ

createParamDecorator にファクトリ関数を渡すだけで引数デコレータが作れるという骨格を、固定値を返す最小例で示します。ハンドラの引数に @Hello() と書くと、ファクトリが返した値がそのまま入ってきます。

import {
  createParamDecorator, ExecutionContext,
  Controller, Get,
} from "@nestjs/common";

// ファクトリが返した値が、そのまま引数に入る
export const Hello = createParamDecorator(
  (_data: unknown, _ctx: ExecutionContext) => "world",
);

@Controller("demo")
export class DemoController {
  @Get()
  greet(@Hello() word: string) {
    return { message: "hello " + word }; // { message: "hello world" }
  }
}

2リクエストから値を取り出す

ctx.switchToHttp().getRequest() でリクエストを取り出し、そこから必要な情報を返すのがデコレータの基本形です。ここでは User-Agent ヘッダを取り出す @UserAgent() を作り、ガード前提でなくても使えることを示します。

import {
  createParamDecorator, ExecutionContext,
  Controller, Get,
} from "@nestjs/common";

// リクエストヘッダから User-Agent を取り出すデコレータ
export const UserAgent = createParamDecorator(
  (_data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.headers["user-agent"];
  },
);

@Controller("demo")
export class DemoController {
  @Get()
  show(@UserAgent() ua: string) {
    return { userAgent: ua };
  }
}

3data 引数でプロパティ名を受け取る

createParamDecorator の第1引数 data には、@User("id") の "id" のように呼び出し側で渡した値が入ります。data があればそのプロパティだけ、なければオブジェクト全体を返す分岐が、引数つきデコレータの定番の形です。

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

// 認証ガードが request.user に入れた値を取り出す
export const User = createParamDecorator(
  (data: string | undefined, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;
    // @User("id") なら user.id、@User() なら user 全体
    return data ? user?.[data] : user;
  },
);

// 使い分け:
//   me(@User() user) {}        ← オブジェクト全体
//   greet(@User("name") n) {}  ← name プロパティだけ

4data 分岐の本質をプレーンTSで再現

デコレータの裏側で起きている「data があれば1プロパティ、なければ全体を返す」分岐を、デコレータなしの純粋な関数で再現した実行例です。createParamDecorator が特別な魔法ではなく、リクエストから値を選ぶだけの関数であることが分かります。

TypeScript

5実務: @User() でハンドラを薄く保つ

認証済みユーザーをハンドラで使う2つの書き方の対比です。@Req() から req.user を毎回掘り出す代わりに @User() / @User("id") を使うと、ハンドラの引数を見るだけで何を必要としているかが明確になります。

import { Controller, Get, Patch, Body, Req, UseGuards } from "@nestjs/common";

@Controller("profile")
@UseGuards(AuthGuard) // これが request.user を埋める前提
export class ProfileController {
  // 従来: 毎回 @Req() して req.user を掘り出す
  @Get("raw")
  meRaw(@Req() req: { user: { id: string } }) {
    return { id: req.user.id };
  }

  // @User() で意図が明確に。引数を見れば必要な情報が分かる
  @Get()
  me(@User() user: { id: string; name: string }) {
    return user;
  }

  @Patch()
  rename(@User("id") userId: string, @Body("name") name: string) {
    return { userId, name };
  }
}