【Next.js】 App Router の Route Handlers で勝手なものを export してはいけない (Type 'OmitWithTag< ... エラー)




環境は以下です
```
next 14.2.13
react 18.3.1
```
```
> pnpm --version
9.11.0
```

# 要点

app router の `route.ts` で export していいのは以下のものだけです。

```typescript
type T = {
  GET?: Function
  HEAD?: Function
  OPTIONS?: Function
  POST?: Function
  PUT?: Function
  DELETE?: Function
  PATCH?: Function
  config?: {}
  generateStaticParams?: Function
  revalidate?: false | 0 | number
  dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
  dynamicParams?: boolean
  fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
  preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
  runtime?: 'nodejs' | 'experimental-edge' | 'edge'
  maxDuration?: number
}
```

それぞれ以下のいずれかに記載があります。


https://nextjs.org/docs/app/building-your-application/routing/route-handlers


https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config



また上記にあげたものの以外を `export` しようとすると以下のようなエラーがでます。

```
Type error: Type 'OmitWithTag<typeof import("プロジェクトディレクトリ/src/app/api/.../route.ts"), "config" | ... 14 more ... | "PATCH", "">' does not satisfy the constraint '{ [x: string]: never; }'.
  Property 'exportしようとしたもの' is incompatible with index signature.
    Type 'string' is not assignable to type 'never'.

   6 |
   7 | // Check that the entry is a valid entry
>  8 | checkFields<Diff<{
     |             ^
   9 |   GET?: Function
  10 |   HEAD?: Function
  11 |   OPTIONS?: Function
```

# なんでエラーになるか(どういう仕組みか)

`pnpm run build`/`npm run build` などをすると、`プロジェクトディレクトリ/.next/types/app/` 以下に app dir 以下と同様のディレクトリ構成で型情報が生成されます。これを基に Lint をしてくれるわけです。

Route Handlers では api の型情報は以下のようなものが生成されます。

```typescript
// File: .../route.ts
import * as entry from 'your-api-path/route.js'
import type { NextRequest } from 'next/server.js'

type TEntry = typeof import('your-api-path/route.js')

// Check that the entry is a valid entry
checkFields<Diff<{
  GET?: Function
  HEAD?: Function
  OPTIONS?: Function
  POST?: Function
  PUT?: Function
  DELETE?: Function
  PATCH?: Function
  config?: {}
  generateStaticParams?: Function
  revalidate?: RevalidateRange<TEntry> | false
  dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
  dynamicParams?: boolean
  fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
  preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
  runtime?: 'nodejs' | 'experimental-edge' | 'edge'
  maxDuration?: number
  
}, TEntry, ''>>()

// Check the prop type of the entry function
if ('GET' in entry) {
  checkFields<
    Diff<
      ParamCheck<Request | NextRequest>,
      {
        __tag__: 'GET'
        __param_position__: 'first'
        __param_type__: FirstArg<MaybeField<TEntry, 'GET'>>
      },
      'GET'
    >
  >()

// [省略]

// =============
// Utility types
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never

// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>

type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never

type ParamCheck<T> = {
  __tag__: string
  __param_position__: string
  __param_type__: T
}

function checkFields<_ extends { [k in keyof any]: never }>() {}

// [省略]

```

簡単に解説します

- まずこのファイルは普通の ts ファイルです。これを実行することで Lint を行っていると考えられます。
- 上部の `entry`, `TEntry` で `route.ts` で export したものやその型を import しています
- 実際にエラーが出ているのは `checkFields<Diff<{ ... }, TEntry, ''>>()` という部分です
  - この `chechFields()` は空の関数で、これを実行することで実際にはジェネリック型の型チェックをしています。
      ```typescript
      function checkFields<_ extends { [k in keyof any]: never }>() {}
      ```
  - `Diff<>` ですがこれは下部にあるように実体は `Omit<>` です
    > If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as `Omit<T, keyof Base>`.
      ```typescript
      type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
      type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
      ```
    - `Diff<Base, T, Message>` で `Omit<T, keyof Base>` になるので、`T` の中から `Base` に存在しないものを抜き出します。

  - つまり `'T の中から Base に存在しないもの' extends { [k in keyof any]: never }` となります。どのような property も never を満たすことはできないので結局はエラーになります。
- 結局、やっていることは "export しているものは(最初にあげたものの)条件を満たすか" ということでした。


こうやって見てみると少しだけ大回りな方法でチェックをしているように感じました。(確かにファイルはまあまあな大きさで、これ以外にもチェックしているものはあります.)

ですが NextJS がやっている事なので、このようなやり方にしていることには何らかの意図があるのでしょう。(もしいきさつなどを知っている方がいたら教えてください!)

# 最後に

自分はなぜこのエラーが起きるのか、公式ドキュメントを見て回ったのですが、export できるものに制限があるとは書いてなかったと思います(自分が見逃しているだけ?)。

まあ api を設定しているファイルでハンドラや設定以外を export する人なんていないんでしょうが...


# 参考文献

- ["TypeScriptのtypeof import("...")とは #TypeScript - Qiita". https://qiita.com/suin/items/dbe5d8ccb935df888b26.](https://qiita.com/suin/items/dbe5d8ccb935df888b26) (Cited on: 2024-11-04).

- ["Routing: Route Handlers | Next.js". https://nextjs.org/docs/app/building-your-application/routing/route-handlers.](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) (Cited on: 2024-11-04).
- ["File Conventions: Route Segment Config | Next.js". https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config.](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config) (Cited on: 2024-11-04).