openapi-typescriptでFile型を扱う際にハマったこと

# はじめに
こちらは株式会社CLAVESアドベントカレンダーの関連記事です。
https://qiita.com/advent-calendar/2024/claves

# openapi-typescriptとは
[openapi-typescript](https://openapi-ts.dev/ja/)は、OpenAPI/Swagger仕様書からTypeScriptの型定義を自動生成してくれるライブラリの一つです。
プロジェクトでこちらのライブラリを採用した際に、つまづいた点について残しておきます。

# openapi-typescriptでFileを扱う際のトラブル
以下のようなopenapi定義を例に挙げます。
一般的なファイルアップロードのリクエストボディの定義です。
```yaml
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                filename:
                  type: string
                  description: Name of the file
                file:
                  type: string
                  format: binary
                  description: The image file to upload
              required:
                - file
```
こちらを元に、[ドキュメント](https://openapi-ts.dev/ja/introduction#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9)の通りに`npx openapi-typescript`コマンドで型生成を行うと、以下のような型が生成されます。

```
        requestBody: {
            content: {
                "multipart/form-data": {
                    /** @description Name of the file */
                    filename?: string;
                    /**
                     * Format: binary
                     * @description The image file to upload
                     */
                    file: string;
                };
            };
        };
```
fileについて、string型で生成されているのがわかると思います。
Format: binaryはコメント状では記載がありますが、型生成上は無視されてしまっています。

これについては、以下のissue内で公式からのコメントがありました。

「[Support for File Upload #1214](https://github.com/openapi-ts/openapi-typescript/issues/1214)」

つまるところ、
- openapi-typescriptはformatプロパティを基本的に無視している。
- この原則から逸脱したかったら、[transform API](https://openapi-ts.pages.dev/node#transform--posttransform)を用意しているから使ってね。

ということのようです。
# 対応策
`format: binary`について、Blobとして扱いたいケースもあると思うので、対応していきましょう。
## 型生成
[transform APIのドキュメント](https://openapi-ts.pages.dev/node#example-blob-types)を参考に、用いて、型生成のスクリプトをカスタマイズします。
```typescript
// src/api/script/generate-api-types.ts
import fs from 'node:fs'

import openapiTS, { astToString } from 'openapi-typescript'
import path from 'path'
import { factory } from 'typescript'

const BLOB = factory.createTypeReferenceNode(factory.createIdentifier('Blob'))

async function generateTypes() {
  const ast = await openapiTS(
    // OpenAPI スキーマファイルのパスを指定
    new URL(
      '../../../openapi.yaml',
      import.meta.url,
    ),
    {
      transform(schemaObject) {
        // binary format の場合、Blob型に変換
        if (schemaObject.format === 'binary') {
          return BLOB
        }
      },
    },
  )

  const contents = astToString(ast)

  // 生成したい場所にファイルを出力
  fs.writeFileSync(path.resolve(process.cwd(), './src/api/index.ts'), contents)
}

generateTypes()
```
実際に実行します。
```
npx tsx src/api/script/generate-api-types.ts
```
出力結果は以下です。
```typescript
        requestBody: {
            content: {
                "multipart/form-data": {
                    /** @description Name of the file */
                    filename?: string;
                    /**
                     * Format: binary
                     * @description The image file to upload
                     */
                    file: Blob;
                };
            };
        };
```
`Format: binary`が指定されたfileについて、`Blob`として型生成されました。

## openapi-fetchでのリクエスト時
`openapi-typescript`を用いる際、[`openapi-fetch`](https://openapi-ts.pages.dev/openapi-fetch/)を共に採用するケースが多いと思います。

openapi-fetchで、今回のようなファイルアップロードAPIへリクエストする際にもハマりどころがあります。

それは、openapi-fetchではデフォルトだと `multipart/form-data`指定がopenapi定義上に存在しても、`JSON.stringify`されてしまう点です。

`multipart/form-data` を使用する場合はbodySerializerを使うようにドキュメントに指示があるので、そちらに沿ってリクエストを作成してみましょう。

```typescript
  const { data, error } = await apiClient.POST("/upload/image", {
    body: {
      filename: imageName,
      file: image,
    },
    bodySerializer(body: UploadBody) {
      const formData = new FormData();
      if (body.filename) {
        formData.append("filename", body.filename);
      }
      formData.append("file", body.file);
      return formData;
    },
  });
```

これで`multipart/form-data`を正常に扱えるようになるはずです。

# 参考
https://github.com/openapi-ts/openapi-typescript/issues/1214