👌

ジェネリクスを簡単に理解する

2022/04/18に公開

ジェネリクスとは

ジェネリクスとは、一言でいうと型を動的にするものです。

動的にするとは

みなさんは普段、関数に引数を設けることで関数を動的にしています。

const f = (x) => typeof x;
f(1); // 'number'
f('hoge'); // 'string'

関数fは引数にxを取り、その値によって戻り値が動的に変化します。
TypeScriptの型においても、ジェネリクスを使うことでこれと同様のことを実現できます。

型を動的にする

関数の引数と同様に、ジェネリクスでは型の引数を設けることで型を動的にします。

const f = <T>(x: T): string => typeof x;
f<number>(1); // xはnumber型

<T>が型の引数であり、Tの値によって関数の引数や戻り値が動的に変化します。
上記の例では、関数の呼び出し時にf<number>としているので、fはnumber型の引数xを取り、戻り値がstring型の関数となります。

型変数のデフォルト値

型変数にはデフォルト値を設けることができます。関数の呼び出し時に型を指定しなければデフォルト値が代入され、型を指定すればその値が代入されます。

const f = <T = number>(x: T): string => typeof x;
f(1); // 型を指定していないのでxはデフォルト値のnumber型
f<string>('hoge'); // 型を指定しているのでxはstring型

型変数に代入できる型を制限する

{型変数} extends {代入を許可する型}の形で型変数に代入できる型を制限することができます。
代入を許可する型はユニオン型で複数指定可能です。

const f = <T extends string | number>(x: T): string => typeof x;
f<number>(1); // number型の代入は許可されている
f<boolean>(true); // boolean型の代入は許可されていないのでコンパイルエラーとなる

ちなみに型変数のデフォルト値と組み合わせるとこのようになります。

const f = <T extends string | number = string>(x: T): string => typeof x;
f(1); // デフォルト値はstring型なのでコンパイルエラーとなる
f<boolean>(true); // boolean型の代入は許可されていないのでコンパイルエラーとなる

実践編:コードリーディング

以上の知識を踏まえて、axiosのソースコードを読んでみます。

axios.get
export class Axios {
  get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
}

axiosgetメソッドは、型引数を3つ取ります。

T

Tはデフォルト値がanyで、AxiosResponseの型引数として渡されます。型引数をさらに別の型の型引数として渡しているということですね。(ややこしい)

AxiosResponseの型定義を見てみるとdataの型がTとなっています。つまりTには、APIをコールして得たデータの型を代入すれば良いとわかります。

AxiosResponse
export interface AxiosResponse<T = any, D = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: AxiosResponseHeaders;
  config: AxiosRequestConfig<D>;
  request?: any;
}

R

Rはgetメソッドの戻り値であるPromiseの型(Promise<R>)として渡されます。

また、RにはAxiosResponse<T>がデフォルト値として指定されています。通常はTを使用してAPIをコールして得るデータの型を指定するので、Rをあえて指定することは少ないと思います。

D

Dもデフォルト値はanyで、AxiosRequestConfigの型引数として渡されます。こちらもAxiosResponseと同様で、Dは最終的にAxiosRequestConfig.dataの型となっていることがわかります。

AxiosRequestConfig
export interface AxiosRequestConfig<D = any> {
  url?: string;
  method?: Method;
  baseURL?: string;
  transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
  transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
  headers?: AxiosRequestHeaders;
  params?: any;
  paramsSerializer?: (params: any) => string;
  data?: D;
  timeout?: number;
  timeoutErrorMessage?: string;
  withCredentials?: boolean;
  adapter?: AxiosAdapter;
  auth?: AxiosBasicCredentials;
  responseType?: ResponseType;
  xsrfCookieName?: string;
  xsrfHeaderName?: string;
  onUploadProgress?: (progressEvent: any) => void;
  onDownloadProgress?: (progressEvent: any) => void;
  maxContentLength?: number;
  validateStatus?: ((status: number) => boolean) | null;
  maxBodyLength?: number;
  maxRedirects?: number;
  socketPath?: string | null;
  httpAgent?: any;
  httpsAgent?: any;
  proxy?: AxiosProxyConfig | false;
  cancelToken?: CancelToken;
  decompress?: boolean;
  transitional?: TransitionalOptions;
  signal?: AbortSignal;
  insecureHTTPParser?: boolean;
}
GitHubで編集を提案

Discussion