Factory 関数の引数は Object にしてほしいという話

自明だと思っていたけれど、割と見かけるので。

Factory 関数やそれに相当するようなものは、得てして作成時点、あるいは将来的にかなりのオプションを取る可能性があります。

例えば HTTP クライアント。

初期開発では環境変数で切り替えられる baseURL の設定があるだけで良かったものが、場合に応じて Authorization Header が必要であったり不要であったり、何かしらの nonce を付与したかったり、 Content-Type が 'application/json' ではなかったりするケースに直面する。というのは、 SPA を開発しているとよくあるシチュエーションではないでしょうか。

こういったときに、引数が 2,3 のときに妥協して createAPIClient(baseUrl: string, token?: string) みたいな作り方をしてしまっている場合、引数を追加および削除する場合に、かなり影響範囲が大きくなってしまいます。

こういった利用箇所が広いもののファクトリ関数を作成する時は、はじめから引数をオプションで受け取っておくことを推奨します。

たとえば、 HTTP クライアントでは、実際に axios がこの手法を採用しています。axios のクライアントを作成するための関数 axios.create では、以下のような膨大なオプションを持つ、 Optional な単一の引数を取ります。

// https://github.com/axios/axios/blob/master/index.d.ts#L44-L74
export interface AxiosRequestConfig {
  url?: string;
  method?: string;
  baseURL?: string;
  transformRequest?: AxiosTransformer | AxiosTransformer[];
  transformResponse?: AxiosTransformer | AxiosTransformer[];
  headers?: any;
  params?: any;
  paramsSerializer?: (params: any) => string;
  data?: any;
  timeout?: number;
  withCredentials?: boolean;
  adapter?: AxiosAdapter;
  auth?: AxiosBasicCredentials;
  responseType?: string;
  xsrfCookieName?: string;
  xsrfHeaderName?: string;
  onUploadProgress?: (progressEvent: any) => void;
  onDownloadProgress?: (progressEvent: any) => void;
  maxContentLength?: number;
  validateStatus?: (status: number) => boolean;
  maxRedirects?: number;
  httpAgent?: any;
  httpsAgent?: any;
  proxy?: AxiosProxyConfig | false;
  cancelToken?: CancelToken;
}

実際に利用する際は、必要なものを必要なだけ定義します。

import axios from 'axios'

export const APIClient = axios.create({
  baseURL: process.env.BASE_URL || 'http://localhost:8080/api/v1'
})

こんな感じにしておくと、不要になったオプションが出た場合も、使っていない部分は影響がなく、また、使っている部分もファクトリ関数側で取り扱いを柔軟にハンドリングできます。

import axios from 'axios'

export type Config = {
  baseURL: string
  nonce?: string
}

export const create(config: Config) {
  if (!config) {
    return axios.create()
  }
  if (config.nonce) {
    if (process.env.NODE_ENV !== 'production') {
      console.warn('nonce option is depcated.')
    }
  }
  return axios.create({...config})
}

こんな感じ。