環境は以下です
next 14.2.13
react 18.3.1
> pnpm --version
9.11.0
要点
app router の route.ts
で export していいのは以下のものだけです。
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
}
それぞれ以下のいずれかに記載があります。
また上記にあげたものの以外を 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 の型情報は以下のようなものが生成されます。
// 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()
は空の関数で、これを実行することで実際にはジェネリック型の型チェックをしています。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>
.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. (Cited on: 2024-11-04).
-
"Routing: Route Handlers | Next.js". 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. (Cited on: 2024-11-04).