Nuxt3のuseFetchの型定義を探索してみたら結構面白かった話
この記事は個人ブログと同じ内容です
先日10/12にNuxt3がpublic betaになりました!🎉
Nuxt2から抜本的に変更されたNuxt3では面白い変更点が多いのですが、今回はuseFetchの挙動に関して探索してみようと思います。
useFetchとは?
Data Fetchingにて紹介されている非同期データ取得APIのうちの一つ。
useFetch(url: string, options?)
というような形で呼び出す、非常にシンプルなAPIではあるのですが、useAsyncData
や $fetch
のラッパーであったり、自動生成されたローカルAPIのレスポンスの型を提供するということで、型定義はかなり複雑な形になっています。
まずは型定義から
useFetchの型定義は下記のようになっています。
export declare function useFetch<ReqT extends string = string, ResT = FetchResult<ReqT>, Transform extends (res: ResT) => any = (res: ResT) => ResT, PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>>(url: ReqT, opts?: UseFetchOptions<ResT, Transform, PickKeys>): import("./asyncData").AsyncData<import("./asyncData").PickFrom<ReturnType<Transform>, PickKeys>>;
型パズル感すごいですね…
ここからは一つずつ紐解いていくことにします
Transform剥がし
Transform
はuseAsyncDataのオプションの一つである、返り値の変換器であるため、一旦削ってシンプルにしてみます。
export declare function _useFetch<ReqT extends string = string, ResT = FetchResult<ReqT>, PickKeys = KeysOf<ResT>>(url: ReqT, opts?: UseFetchOptions<ResT, (input: ResT)=> ResT, PickKeys>): import("./asyncData").AsyncData<import("./asyncData").PickFrom<ResT, PickKeys>>;
※この状況だとUseFetchOptionsの第3型引数がエラーになってしまいますが、型の抽象度を意図的に変えてしまったのが原因なので一旦無視します
返り値に注目
ここで返り値に注目してみます。
asyncData.d.ts
では下記のように定義されているため
// Tのうち、Kの配列の要素に指定されたkeyの要素だけを抽出して取り出す
export declare type PickFrom<T, K extends Array<string>> = T extends Record<string, any> ? Pick<T, K[number]> : T;
...
// Dataの型を、asyncDataが返してくれる型に変換する
export interface _AsyncData<DataT> {
data: Ref<DataT>;
pending: Ref<boolean>;
refresh: (force?: boolean) => Promise<void>;
error?: any;
}
export declare type AsyncData<Data> = _AsyncData<Data> & Promise<_AsyncData<Data>>;
返り値は「ResT
から PickKeys
で指定した要素だけをとりだし、asyncDataが返してくれる形に変換した型」となります。
ResTの探索
では ResT
も探索してみます。
ResT = FetchResult<ReqT>;
export declare type Awaited<T> = T extends Promise<infer U> ? U : T;
export declare type FetchResult<ReqT extends string> = Awaited<ReturnType<$Fetch<unknown, ReqT>>>;
となっているため、「ResT
は$Fetch<unknown, ReqT>
の返り値のPromiseを剥がしたもの、もしくはそれ自体」となります。
$Fetchの探索
$Fetch
を見てみると下記の様になっています。
export declare interface $Fetch<T = unknown, R extends FetchRequest = FetchRequest> {
(request: R, opts?: FetchOptions): Promise<TypedInternalResponse<R, T>>
raw (request: R, opts?: FetchOptions): Promise<FetchResponse<TypedInternalResponse<R, T>>>
}
ResT
を当てはめResT
の算出に必要なものだけを残すと
export declare interface $Fetch<unknown, ReqT> {
(request: R, opts?: FetchOptions): Promise<TypedInternalResponse<ReqT, unknown>>
}
となります。
TypedInternalResponse
を見ていくと下記になっているので
export declare type TypedInternalResponse<Route, Default> =
Default extends string | boolean | number | null | void | object
// Allow user overrides
? Default
: Route extends string
? MiddlewareOf<Route> extends never
// Bail if only types are Error or void (for example, from middleware)
? Default
: MiddlewareOf<Route>
: Default
今回の型で置き換えると
export declare type TypedInternalResponse<ReqT, unknown> =
ReqT extends string
? MiddlewareOf<ReqT> extends never
// Bail if only types are Error or void (for example, from middleware)
? unknown
: MiddlewareOf<ReqT>
: unknown
となります。neverを一旦無視するとMiddlewareOf<ReqT>
が返ってくるといえそうです。
残りの型定義は下記となるので
export declare interface InternalApi { }
export declare type ValueOf<C> = C extends Record<any, any> ? C[keyof C] : never
export declare type MatchedRoutes<Route extends string> = ValueOf<{
// exact match, prefix match or root middleware
[key in keyof InternalApi]: Route extends key | `${key}/${string}` | '/' ? key : never
}>
export declare type MiddlewareOf<Route extends string> = Exclude<InternalApi[MatchedRoutes<Route>], Error | void>
MiddlewareOf<ReqT>
に当てはめて考えてみると
export declare interface InternalApi { }
export declare type ValueOf<C> = C extends Record<any, any> ? C[keyof C] : never
export declare type MatchedRoutes = ValueOf<{
// exact match, prefix match or root middleware
[key in keyof InternalApi]: ReqT extends key | `${key}/${string}` | '/' ? key : never
}>
export declare type MiddlewareOf = Exclude<InternalApi[MatchedRoutes<ReqT>], Error | void>
となります。ざっくりいうと、「InternalApi
の中にReqT
がキーとなる物があればそれのValueを返却する」というふうに解釈できますね。
つまり「ResT
はInternalApi
のキーがReqT
に相当するもののValueの型」と推察できます。
InternalApiの自動生成
上記型定義だとInternalApi
はデフォルトで {}
です。つまりこのままではなんの意味もないものになります。
この型定義をoverwriteしてくれるのがnitro
というNuxt3のサーバーエンジンです。
nitro
は色々な機能があるのですが、そのうちの一つが /server/
ディレクトリに配置した関数の返り値を解釈し、型定義を生成してくれるというものです。
例えば/server/api/count.ts
という下記のTSファイルを設置してみます。
let counter = 0;
export default (): { counter: number } => {
counter++;
return { counter };
};
すると.nuxt/nitro.d.ts
という、下記の内容のファイルが生成されます。
declare module '@nuxt/nitro' {
interface InternalApi {
'/api/count': ReturnType<typeof import('../server/api/count').default>
}
}
export {}
InternalApi
が拡張され、/api/count
に対する型定義が出現しました。
これにより、「ResT
はInternalApi
の中にあるReqT
に相当するもののValueの型」は
「ResT
はReqT
が/api/count
のとき/server/api/count
の返り値」となることができました。
useFetchの返り値
ここでuseFetchを実際に使ってみてこの効果を探ってみます。
const {data} = await useFetch('/api/count')
としてみたとき、data
の型はシンプルに
Ref<Pick<{
counter: number;
}, "counter">>
となります。
useFetchでは単にエンドポイントを指定しているだけに過ぎないのですが、その返り値がVue3で用いやすい型として抽出されている事がわかります。
InternalApi
に型定義がなくても使えるような設計になっているため、useFetch
の引数に対しての補完が効かないのが難点ではありますが、Nuxtのディレクトリ構造を探索すればファイル名だけである程度予想はできるので、便利に使えそうです。
とりあえず一旦まとめ
useFetchの返り値を探索するだけで結構長くなったので、一旦このへんで終わりにします。
useFetchはこれ以外にもuseAsyncData
や$fetch
のオプションもサポートしているので、よかったら探索してみてください。オプションのサポートは上記に比べると大したことはないので、気構えずに見れると思います
Discussion