openapi-generator-cli による TypeScript 型定義
openapi-generator-cli を利用すると、OpenAPI 定義から TypeScript で利用可能なアセットを生成することができます。しかし最適な成果物を得るためには、OpenAPI 定義自体に少し工夫が必要で、一筋縄にはいかないことがあります。
本稿及びサンプルリポジトリでは、openapi-generator-cli と TypeScript 4.2 の新機能を活用したアプローチを紹介します。
課題点の確認
OpenAPI 定義にあたりresponses
フィールドのschema
に対し、インラインで定義を追加することが多いのではないでしょうか?OpenAPI を定義する GUI 「Stoplight Studio」 などでは、このインラインスキーマ定義が直感的で使いやすく、yaml を直接編集しなくても良いなど、開発に重宝します。
responses:
"200":
description: OK
content:
application/json:
schema:
description: ""
type: object
properties:
id:
type: string
name:
type: string
age:
type: number
gender:
type: string
required:
- id
- name
- age
しかしながらこの定義のままでは、下記InlineResponse200
の様な、望まない型定義出力となってしまいます。この連番型定義では何を指しているのか不明瞭で、動的に変更してしまうリスクも伴い、使い物になりません。
/**
*
* @export
* @interface InlineResponse200
*/
export interface InlineResponse200 {
/**
*
* @type {string}
* @memberof InlineResponse200
*/
id: string;
/**
*
* @type {string}
* @memberof InlineResponse200
*/
name: string;
/**
*
* @type {number}
* @memberof InlineResponse200
*/
age: number;
/**
*
* @type {string}
* @memberof InlineResponse200
*/
gender?: string;
}
これを乗り越えるためには、別途 Model 定義を用意し、$ref
参照しなければいけません。この課題のためだけに全て Model 定義をするとなると、工数に見合わなくなることもあります。本稿の主旨はこの 「インラインスキーマ定義はそのままで、型定義のみ抽出する」 ことです。
一意の operationId は必ず付与・管理すること
openapi-generator-cli の typescript-axios を使う場合、成果物として ApiFactory 関数を得ることができます(以下DefaultApiFactory
関数)ApiFactory 関数で生成された axios fetcher は、OpenAPI 定義内訳が伝搬したヘルパー関数です。さきのInlineResponse200
などが型注釈されており、この axios fetcher を使う限りでは型安全を担保することができます。
import { DefaultApiFactory } from "../openapi/dist/api";
export const api = DefaultApiFactory();
export async function main() {
try {
const { data } = await api.usersGet();
const usersId = data.users.map((user) => user.id);
console.log(usersId);
} catch (err) {
console.log(err);
}
}
この axios fetcher、await api.usersGet()
の関数名usersGet
は、OpenAPI 定義のoperationId
に由来します。 第一関門として、一意の operationId は必ず付与しなければいけません。 operationId の管理がおざなりになっている場合、まずはここから正す必要があります。
axios fetcher を使いたくない場合
axios fetcher を利用したくないケースもあるかもしれません。(axios fetcher が重厚すぎる、サポート対象環境で不具合が発生する、など)そうなると、TypeScript コードで恩恵を受けることが出来なくなります。そこで、TypeScript の type inference in conditional types(いわゆる型パズル)の登場です。
ResponseBody 型の抽出
以下の型パズルを利用すると、axios fetcher の全 ResponseBody を抽出することができます。戻り値型注釈を抽出するだけなので、特別目新しいものではありません。
export const api = DefaultApiFactory();
type InferPromise<T> = T extends (...arg: any[]) => Promise<infer I>
? I
: never;
type InferAxiosResonse<T> = T extends AxiosResponse<infer I> ? I : never;
type InferResponseBody<T> = {
[K in keyof T]: InferAxiosResonse<InferPromise<T[K]>>;
};
export type ResponseBody = InferResponseBody<typeof api>;
// ______________________________________________________
//
// operationId 由来の関数名で添字アクセスする
//
type T1 = ResponseBody["usersGet"];
type T2 = ResponseBody["usersUserIdGet"];
RequestBody 型の抽出
以下の型パズルを利用すると、axios fetcher の全 RequestBody を抽出することができます。axios fetcher は、「最後から二番目の引数が RequestBody 相当」 という特徴をもっており、そこに目をつけています。
export const api = DefaultApiFactory();
type InferSecondFromBack<T> = T extends [...any[], infer U, any] ? U : never;
type InferFunctionArgs<T> = T extends (...arg: infer U) => any
? Required<U> // optional の場合抽出できないため、Required変換
: never;
type InferRequestBody<T> = {
[K in keyof T]: InferSecondFromBack<InferFunctionArgs<T[K]>>;
};
export type RequestBody = InferRequestBody<typeof api>;
// ______________________________________________________
//
// operationId 由来の関数名で添字アクセスする
//
type T3 = RequestBody["usersUserIdPatch"];
type T4 = RequestBody["usersPost"];
これまで「最後から n 番目の引数」の様な型抽出は不可能でしたが、TypeScript 4.2から抽出が可能になりました。InferSecondFromBack<InferFunctionArgs<T[K]>>
の様に指定することで「最後から二番目の引数」を導出することができます。
複雑な型定義はこの様な局面で役に立つことがありますので、覚えておくと便利です。
Discussion