📝

NuxtのuseFetchのカスタマイズが思いのほか面倒だった

に公開

useFetchbaseURLオプションを毎度書くのが非常に面倒なので、ラッパーコンポーザブルをサクッと書こうとしたら意外難しかったのでメモ。

まずnuxt.config.tsbaseURLオプションを指定しておく。

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public:{
      baseURL: "http://example.com/",
    }
  }
}

ラッパーコンポーザブルuseApi.tsを試しに👇のように書くと、useFetchにオプションを渡すときに、型の不一致を起こす。どうやらUseFetchOptions<T>の型引数の指定が甘いらしい。

composables/useApi.ts
import type { UseFetchOptions } from "#app"
import type{FetchError} from "ofetch"

type Req= string|Ref<string>|(()=>string)

function useApi<T, ErrorT=FetchError>(
  url: Req, opts?: UseFetchOptions<T> //<--型引数の指定が甘い
){
  const {public: {baseURL}}= useRuntimeConfig()
  const fetchOpts= { ...opts, baseURL}
  return useFetch<T, ErrorT>(url, fetchOpts) //<--❌fetchOptsで型の不一致を起こす
}

export default useApi

UseFetchOptionsの型定義は、やたらと込み入っていて、しかもexportされていない型もあったりして非常に厄介、とくにmethodがらみの引数のあたりは非常に込み入っているので簡略化し、以下のように型引数を指定してみる。とりあえず型のエラーは消えた。

composables/useApi.ts
import type { UseFetchOptions } from "#app"
import type{FetchError} from "ofetch"
import type {AvailableRouterMethod} from "nitropack"

type Req= string|Ref<string>|(()=>string)

//メソッドの型は簡略化
type Method= AvailableRouterMethod<string>
type KeysOf<T> = Array<T extends T ? keyof T extends string ? keyof T : never : never>

// ラッパー関数に型引数DefaultTを追加
function useApi<T, ErrorT=FetchError, DefaultT= undefined>(
  url: Req, 
  opts?:  UseFetchOptions<
    //省略せず型引数を全て指定する
    T, T, KeysOf<T>, DefaultT, string, Method
  > 
){
  const {public: {baseURL}}= useRuntimeConfig()
  const fetchOpts= { ...opts, baseURL}
    return useFetch<
      //こちらも全て指定
      T, ErrorT, string, Method, T, T, KeysOf<T>, DefaultT
    >(url, fetchOpts) 
}

export default useApi

しかし、これをコンポーネントで使おうとすると、defaultオプションで型エラーが起きてしまう。むむむ。

example.vue
type Book={name: string}

//❌defaultオプションで型エラーになる
const {data}= useApi<Book[]>("/hogehoge/books", {default: ()=>[]) 

もう一度useFetchの型定義を確認すると、なんとオーバーロードされているではないか。2つの違いは型引数DefaultTのデフォルト値だけ。要は、defaultオプションの有無によって返される型が異なるため2つのシグニチャーが必要になっている、と理解。

https://github.com/nuxt/nuxt/blob/c4317e057c5a3cac3ea6c96751b84509a689b6f3/packages/nuxt/src/app/composables/fetch.ts#L51-L63

https://github.com/nuxt/nuxt/blob/c4317e057c5a3cac3ea6c96751b84509a689b6f3/packages/nuxt/src/app/composables/fetch.ts#L64-L76

というわけで、useApiも、2つのシグニチャーを用意する。めっちゃ冗長だが、これだと、コンポーネント側でも型推論がうまくいく。

useApi.ts
import type { UseFetchOptions } from "#app"
import type{FetchError} from "ofetch"
import type {AvailableRouterMethod} from "nitropack"

type Req= string|Ref<string>|(()=>string)

type Method= AvailableRouterMethod<string>
type KeysOf<T> = Array<T extends T ? keyof T extends string ? keyof T : never : never>

// オプションの型はここで宣言
type Options<T, DefaultT>= UseFetchOptions<
  T, T, KeysOf<T>, DefaultT, string, Method
>
// 戻り値の型を宣言
type ReturnT<T, ErrorT, DefaultT>= ReturnType<typeof useFetch<
  T, ErrorT, string, Method, T, T ,KeysOf<T>, DefaultT>
>

// シグニチャー1(Default = undefined)
function useApi<T, ErrorT= FetchError, DefaultT= undefined>(
  url: Req, opts?: Options<T, DefaultT> 
):ReturnT<T, ErrorT, DefaultT>

// シグニチャー2(Default = T)
function useApi<T, ErrorT= FetchError, DefaultT= T>(
  url: Req, opts?: Options<T, DefaultT> 
):ReturnT<T, ErrorT, DefaultT>

// 関数本体
function useApi<T, ErrorT=FetchError, DefaultT= undefined>(
  url: Req, opts?: Options<T, DefaultT> 
){
  const {public: {baseURL}}= useRuntimeConfig()
  const fetchOpts= { ...opts, baseURL}
  return useFetch<
    T, ErrorT, string, Method, T, T, KeysOf<T>, DefaultT
  >(url, fetchOpts) 
}

export default useApi

それにしても、冗長なコード。何とかならないものか。

Discussion