TypeSpecアドカレ記事です!
結論
TypeSpecのEmitterでOpenAPI 3.1.0を指定するとRecord<unknown>がunevaluatedPropertiesとして評価され、TypeScriptへの変換時にRecord<string, never>として解釈されてしまうので3.0.0を利用する。
3.0.0に戻すとadditionalPropertiesとして評価され、TypeScriptでは Record<string,unknown>として解釈される。
前提
利用ライブラリ
- TypeSpec
- OpenAPI 3.0.0/3.1.0
- OpenAPI TypeScript
背景
TypeSpecを利用し、以下のような流れで型・IFの自動生成を行おうとした。
TypeSpec->OpenAPI
OpenAPI->TypeScript
OpenAPI->Python
APIのレスポンスには以下のような動的キーのObject配列が含まれていたため、連想配列のスキーマ定義が必要となった。
const response = {
"KEY-001": {
name: "Sample001",
key: "49vksdx9a3kf",
//以降いくつかのプロパティ
},
"KEY-002": {
name: "Sample002",
key: "95dgf9gdrd",
//...
},
//...
}
連想配列のキーは動的に生成されるため、TypeScriptでは以下のような型が期待される。
type ResponseItem = {
name: string,
key: string,
//...
}
type Response = Record<string, ResponseItem>
ケースの説明
上記のような型を生成するにはTypeSpecで以下のように定義する必要がある。
//model.tsp
model ResponseItem {
name: string;
key: string;
//...
}
model Response = Record<ResponseItem>
しかし、APIの仕様上レスポンスに含まれるプロパティが動的に変動する可能性がある場合があったため以下のように定義を変更した。
model ResponseItem {
name: string;
key: string;
...Record<unknown>;
}
決まったkey/valueで返却されるプロパティと動的プロパティ両方を持ち、nameやkeyは推論できるようにしつつ柔軟に他のプロパティも受け入れることが目的だった。
期待するTypeScriptの生成型は以下のとおり。
export interface components {
schemas: {
ResponseItem: {
name: string;
key: string;
} & {
[key: string]: unknown;
}
}
}
起きた問題
TypeSpecのモデル定義に Record<unknown>が含まれている場合、OpenAPI TypeScriptで生成される型が Record<string, never> になってしまう。
問題の原因
OpenAPI TypeScriptの仕様を調べる
OpenAPI TypeScriptでは unevaluatedProperties: {}のオプションを持つschemasを Record<string, never>として変換する仕様となっている。
additionalProperties: {}を持っていれば上記で期待した通りの型が生成される。
TypeSpecからOpenAPIへの変換時の挙動を調べる
以下の記事ではOpenAPI3.1.0ではRecord<unknown>の評価時、additionalPropertiesの代わりにunevaluatedPropertiesを利用している旨が記載されている。
Note: unevaluatedProperties is used instead of additionalProperties when emitting Open API 3.1 specs.
しかし、additionalPropertiesとunevaluatedPropertiesは別モノなのでOpenAPI TypeScriptでは明確に変換時の振る舞いが変わってしまう。
additionalPropertiesとunevaluatedPropertiesの違い
additionalProperties→OpenAPI3.0.0から存在する属性。falseを指定するとallOf等で統合する際、子スキーマ側で拡張したプロパティを許可されていないプロパティとみなしてしまうことがある。
unevaluatedProperties→OpenAPI3.1.0から登場した属性。対象の階層で検証が行われたすべてのプロパティを「既知」とみなし、検証済みの全プロパティが既知とみなされる。
allOfで起きる上記の問題を解決している。
ただし計算コストはやや高め。
対策
TypeSpecのemitterを3.0.0に戻す
3.0.0のemitterであれば Record<unknown>をadditionalPropertiesとして評価してくれるので、現在はこれが手っ取り早い。
3.2.0以降でunevaluatedPropertiesの扱いがどうなるか、OpenAPI TypeScript側での解釈がどう変わるか引き続き注目する。