🌀

【NestJs】ExecutionContextにジェネリクスを渡して型を付けるまで

2024/12/20に公開

https://qiita.com/advent-calendar/2024/tskaigi

はじめに

toridoriのfujitani soraです
TsKaigi運営チームにも参加させていただいています。
2/5にshibuya.tsというTypeScriptのイベントを主催させていただくので、そちらもチェックしていただけるとうれしいです🐶

ExecutionContextとgetContext

@nestjs/commonからimportすることができ、NestJs内の現在の実行プロセスに関するContext情報と、ライフサイクルの中で追加したMetadataを保持するクラスです。
getContextメソッドでは、実行コンテキストのリクエスト情報を取得できます。

exec(context: ExecutionContext): void {
    const gqlCtx = GqlExecutionContext.create(context);
    const ctx = gqlCtx.getContext()
    console.log(ctx);
}
log
{
  req: <ref *1> IncomingMessage {
    _events: {
      close: undefined,
      error: undefined,
      data: undefined,
      end: undefined,
      readable: undefined,
      aborted: undefined
    },
    _readableState: ReadableState {
      highWaterMark: 65536,
      buffer: [],
      bufferIndex: 0,
      length: 0,
      pipes: [],
      awaitDrainWriters: null,
      [Symbol(kState)]: 60295036
    },
# ...

このままでもコンテキスト情報の取得は可能ですが、getContext()の記述で値を返すとany型が付きます。
原因を特定し、getContext()に必要な型を付けたい。付けよう。

内部実装を読む

GqlExecutionContextの実装は下記の通りで、Response型をジェネリクスで定義しています。
ジェネリクスが実装されなければanyがGeneric Parameter Defaultsとして定義されています。
故に、getContext()をそのまま返す実装ではany型になっていたわけですね。

@nestjs/graphql/dist/services/gql-execution-context.d.ts
import { ContextType, ExecutionContext } from '@nestjs/common';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { GraphQLArgumentsHost } from './gql-arguments-host';
export type GqlContextType = 'graphql' | ContextType;
export type GraphQLExecutionContext = GqlExecutionContext;
export declare class GqlExecutionContext extends ExecutionContextHost implements GraphQLArgumentsHost {
    static create(context: ExecutionContext): GqlExecutionContext;
    getType<TContext extends string = GqlContextType>(): TContext;
    getRoot<T = any>(): T;
    getArgs<T = any>(): T;
    getContext<T = any>(): T;
    getInfo<T = any>(): T;
}
//# sourceMappingURL=gql-execution-context.d.ts.map

ジェネリクスでResponseに型を付ける

内部実装に倣い、ジェネリクスで型を付けてあげます。
getContextのプロパティはプロジェクトによって違う為、独自の型エイリアスを定義します。

exec(context: ExecutionContext): void {
    const gqlCtx = GqlExecutionContext.create(context);
    const ctx = gqlCtx.getContext<MarketingGqlContext>();
    console.log(ctx);
}
// req, resはNestJs内部のexpressが提供するコンテキスト情報
// loginUser, companyはプロジェクト都合で独自に追加したコンテキスト情報
type CustomGqlContext = {
  req: Request;
  res: Response;
  loginUser: LoginUser;
  company: Company;
};

TypeScriptは「型がドキュメントの役割を果たす」と言われますが、それはライブラリ使用時も同じことです。
コンパイラに怒られる時は、冷静に内部実装の型を読んでいくと理解が深まるのでおすすめです👀

TSKaigi 2025の告知

2025年5月23日/24日にTSKaigi 2025が開催されます!
TSKaigiは日本最大級のTypeScriptをテーマとした技術カンファレンスです(前回の参加者2000人以上)
TypeScriptに興味のある方は、ぜひ公式サイトやXを確認してみてください。

TSKaigi 2025 公式サイト
https://2025.tskaigi.org/

X
https://x.com/tskaigi

Discussion