mizdra's blog

ぽよぐらみんぐ

Relay でも msw の handler を型付きで書く

msw を使うと、GraphQL API をモックしてダミーレスポンスを返せます。以下のような handler と、その他少々のセットアップコードを用意するだけで簡単にモックできます。

// src/mocks/handlers.js
import { graphql } from 'msw';

export const handlers = [
  // ブログのトップページのクエリをモックする handler。
  // `graphql.query` の第一引数には、
  // `query <クエリ名> { ... }` の `<クエリ名>` の部分に対応した文字列を書く。
  graphql.query('BlogTopQuery', (req, res, ctx) => {
    return res(
      ctx.data({
        blog: { title: 'My Blog' },
        entries: [
          { id: '1', title: 'My First Entry' },
          { id: '2', title: 'My Second Entry' },
        ],
      }),
    );
  }),
];

graphql.query はクエリの response の型と variable の型の、2 つの型引数を取る API になってます。それぞれの型引数を渡すと、req.variables や handler の返り値に (静的) 型が付き、型に違反する値を返そうとしたらコンパイルエラーにできます。クエリの response/variable の型定義は、grahpql-codegen とその plugin (@graphql-codegen/typescript@graphql-codegen/typescript-operations) を使ってコード生成できます。

// src/mocks/handlers.js
import { BlogTopQuery, BlogTopQueryVariables } from '../gql/types';
import { graphql } from 'msw';

export const handlers = [
  graphql.query<
    BlogTopQuery,
    BlogTopQueryVariables,
  >('BlogTopQuery', (req, res, ctx) => {
    return res(
      ctx.data({
        blog: { title: 'My Blog' },
        entries: [
          { id: '1', title: 'My First Entry' },
          { id: '2', title: 'My Second Entry' },
        ],
      }),
    );
  }),
];
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  generates: {
  'src/gql/types.ts': {
    plugins: ['typescript', 'typescript-operations'],
      config: {
        flattenGeneratedTypes: true,
      },
  },
};
export default config;

他にも @graphql-codegen/typed-document-node@graphql-codegen/typescript-msw を使えばもう少し短く handler を書けます (詳しくは以下の記事を参照)。

tech.smartshopping.co.jp

the-guild.dev

本題: Relay でも msw の handler を型付きで書きたい

上記のような型付きの handler を、Relay を導入しているプロダクトでもやろうと思うと、実は一筋縄ではいきません。

というのも、Relay に備わっている @argumentsDefinition@refetchable といったディレクティブを、graphql-codegen が正しく処理できないためです。Relay にはいくつかの特別なディレクティブがあるのですが、このうちのいくつかが発行されるクエリやレスポンスの形式に影響を与える可能性があるのです。例えば fragment の定義の中に @refetchable ディレクティブを書くと、その fragment の単位でデータの再 fetch をする用のクエリを新たに定義できます (説明が難しいので詳しくは公式ドキュメントを読んでください)。

relay.dev

一方で graphql-codegen 自体は、そのままでは Relay 固有のディレクティブを正しく理解できません。そのため、Relay を導入しているプロダクトで msw の handler を型付きで書こうにも、クエリの response /variable の型定義がコード生成できず、どうにもらならない...という事態になってしまいます。

解決策: relay-compiler に型定義をコード生成させる

考えてみると当たり前なのですが、Relay の公式グッズなら Relay 向けに書かれたクエリを処理できるので、素直にそれを使えば良いです。relay-compiler という公式 CLI ツールがクエリの定義を読み取って TypeScript 向けのコード生成をしてくれるようになっていて、その中にクエリの response /variable の型定義 (XxxQuery$dataXxxQuery$variables) も含まれています。

ただ、Relay が生成するクエリの response の型定義は、デフォルトだと fragment masking 対応の特別な形式の型定義になっています。クエリが fragment で分割されていると、その fragment で分割されている分の field の型が、クエリの response の型定義に含まれていません。そのため、これをそのまま msw の graphql.query に渡しても、上手く機能しません。

そこで、@raw_response_type ディレクティブの出番です。これをクエリに対して付けると、従来の fragment masking 対応の response の型定義とは別に、fragment masking のない形式の型定義も生成してくれます。

// src/pages/index.tsx
import { graphql } from 'react-relay';

// クエリ名の後ろに `@raw_response_type` を付ける
const BlogTopQuery = graphql`
  query BlogTopQuery @raw_response_type {
    blog {
      ...BlogHeaderFragment
    }
    entries {
      ...EntryCardFragment
    }
  }
`;

export function BlogTopPage() {
  /* ... */
}
// src/mocks/handlers.js

import {
  // `XxxQuery$rawResponse` という名前で fragment masking のない形式の
  // 型定義が生成されているので、import してくる。
  BlogTopQuery$rawResponse,
  BlogTopQuery$variables,
} from '../src/__generated__/BlogTopQuery.graphql';
import { graphql } from 'msw';

export const handlers = [
  // handler に渡す
  graphql.query<
    BlogTopQuery$rawResponse,
    BlogTopQuery$variables,
  >('BlogTopQuery', (req, res, ctx) => {
    return res(
      ctx.data({
        blog: { title: 'My Blog' },
        entries: [
          { id: '1', title: 'My First Entry' },
          { id: '2', title: 'My Second Entry' },
        ],
      }),
    );
  }),
];

別解: @graphql-codegen/relay-operation-optimizer を使う

@graphql-codegen/relay-operation-optimizer というグッズがあって、これを使うと graphql-codegen でも Relay のディレクティブを処理できるようです。

the-guild.dev

しかし「List of Features」をよく見てみると、@argumentsDefinition@arguments には対応しているものの、@refetchable などその他ディレクティブには対応していないようです *1

あまり Relay の機能を駆使していないプロジェクトならこれでもやっていけるかもしれません (とはいえ素直に relay-compiler を使ったほうがトラブルに見舞われなくて良いと思います)。

あとがき

@raw_response_type ですが、API ドキュメントでは全く触れられておらず、用語集のページ *2 や mutation 関連のページ *3 でちょっと出てくるくらいで、全く公式ドキュメントに説明がなくて、ちょっと不安な感じがしますが... とはいえ id:mizdra の手元ではちゃんと動いてそうです。同じように Relay で msw の handler を型付きで書きたい人が居たら参考にしてみてください。

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.