从 OpenAPI 的 `oneOf` 关键字生成不受欢迎的 Flow/TypeScript 类型。是否有另一种 OpenAPI 模式会产生更好的结果?

Undesirable Flow/TypeScript types being generated from OpenAPI's `oneOf` keyword. Is there another OpenAPI schema that would have better results?

我们有以下 OpenAPI 架构:

{
    "openapi": "3.0.1",
    "paths": {
        "/v1/tool/breadcrumbs/{hierarchyId}/{categoryId}": {
            "get": {
                "tags": [
                    "V1-tool"
                ],
                "summary": "Get Breadcrumbs details",
                "operationId": "getBreadcrumbs",
                "parameters": [
                    {
                        "name": "hierarchyId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "minimum": 1,
                            "type": "integer",
                            "format": "int32"
                        }
                    },
                    {
                        "name": "categoryId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "minimum": 1,
                            "type": "integer",
                            "format": "int32"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "default response",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Kitten"
                                    }
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "Auth": []
                    }
                ]
            }
        },
        "/v1/tool/hierarchies": {
            "get": {
                "tags": [
                    "V1-tool"
                ],
                "summary": "Get all available hierarchies ",
                "operationId": "getAllHierarchies",
                "responses": {
                    "200": {
                        "description": "default response",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/HierarchyResponse"
                                    }
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "Auth": []
                    }
                ]
            }
        },
        "/v1/tool/search/{hierarchyId}/{searchTerm}": {
            "get": {
                "tags": [
                    "V1-tool"
                ],
                "summary": "Free text search categories for a given hierarchy",
                "operationId": "searchBy",
                "parameters": [
                    {
                        "name": "hierarchyId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "minimum": 1,
                            "type": "integer",
                            "format": "int32"
                        }
                    },
                    {
                        "name": "searchTerm",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "default response",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Kitten"
                                    }
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "Auth": []
                    }
                ]
            }
        },
        "/v1/tool/category/{hierarchyId}/{categoryId}": {
            "get": {
                "tags": [
                    "V1-tool"
                ],
                "summary": "Get Category data needed to render a Table View in the Category Management tool",
                "operationId": "getTableView",
                "parameters": [
                    {
                        "name": "hierarchyId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "minimum": 1,
                            "type": "integer",
                            "format": "int32"
                        }
                    },
                    {
                        "name": "categoryId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "minimum": 1,
                            "type": "integer",
                            "format": "int32"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "default response",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TableViewResponse"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "Auth": []
                    }
                ]
            }
        }
    },
    "components": {
        "schemas": {
            "Kitten": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "Kitten"
                        ]
                    },
                    "id": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "name": {
                        "type": "string"
                    },
                    "uri": {
                        "type": "string"
                    }
                }
            },
            "HierarchyResponse": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "Hierarchy"
                        ]
                    },
                    "id": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "name": {
                        "type": "string"
                    }
                }
            },
            "AliasCategoryResponse": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "Alias"
                        ]
                    },
                    "level": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "id": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "uri": {
                        "type": "string"
                    }
                }
            },
            "ChildCategoryResponse": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "Category"
                        ]
                    },
                    "level": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "id": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "uri": {
                        "type": "string"
                    }
                }
            },
            "GblSubheaderResponse": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "Translation"
                        ]
                    },
                    "brandCatalogId": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "value": {
                        "type": "string"
                    }
                }
            },
            "Item": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string"
                    }
                },
                "oneOf": [
                    {
                        "$ref": "#/components/schemas/ChildCategoryResponse"
                    },
                    {
                        "$ref": "#/components/schemas/AliasCategoryResponse"
                    },
                    {
                        "$ref": "#/components/schemas/SubheaderResponse"
                    }
                ]
            },
            "ParentCategoryResponse": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "ParentCategory"
                        ]
                    },
                    "id": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "name": {
                        "type": "string"
                    }
                }
            },
            "SubheaderResponse": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "Subheader"
                        ]
                    },
                    "name": {
                        "type": "string"
                    },
                    "translatedNames": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/GblSubheaderResponse"
                        }
                    }
                }
            },
            "TableViewResponse": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "DefaultCategory"
                        ]
                    },
                    "uri": {
                        "type": "string"
                    },
                    "level": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "id": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "parentCategory": {
                        "$ref": "#/components/schemas/ParentCategoryResponse"
                    },
                    "items": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Item"
                        }
                    }
                }
            }
        },
        "securitySchemes": {
            "Auth": {
                "type": "http",
                "description": "Token Authentication  e.g. Bearer <placeholder>",
                "scheme": "bearer",
                "bearerFormat": "JWT"
            }
        }
    }
}

我们运行以下命令从上面的模式文件生成流类型:

npx swagger-to-flowtype path/to/schema/file -d generated_types.js

结果如下:

// @flow strict
export type Kitten = { type: "Kitten", id: number, name: string, uri: string };
export type HierarchyResponse = { type: "Hierarchy", id: number, name: string };
export type AliasCategoryResponse = {
  type: "Alias",
  level: string,
  name: string,
  id: number,
  uri: string
};
export type ChildCategoryResponse = {
  type: "Category",
  level: string,
  name: string,
  id: number,
  uri: string
};
export type GblSubheaderResponse = {
  type: "Translation",
  brandCatalogId: number,
  value: string
};
export type Item = { type: string };
export type ParentCategoryResponse = {
  type: "ParentCategory",
  id: number,
  name: string
};
export type SubheaderResponse = {
  type: "Subheader",
  name: string,
  translatedNames: Array<GblSubheaderResponse>
};
export type TableViewResponse = {
  type: "DefaultCategory",
  uri: string,
  level: string,
  name: string,
  id: number,
  parentCategory: ParentCategoryResponse,
  items: Array<Item>
};

问题是 Item 类型定义为:

export type Item = { type: string };

我们希望将其定义为:

export type Item = ChildCategoryResponse | AliasCategoryResponse | SubheaderResponse;

我们 OpenAPI 架构的相关部分是:

"Item": {
    "type": "object",
    "properties": {
        "type": {
            "type": "string"
        }
    },
    "oneOf": [
        {
            "$ref": "#/components/schemas/ChildCategoryResponse"
        },
        {
            "$ref": "#/components/schemas/AliasCategoryResponse"
        },
        {
            "$ref": "#/components/schemas/SubheaderResponse"
        }
    ]
},

我们最初认为可能 swagger-to-flowtype 在这里有一个错误,但是使用相同的模式通过 swagger-typescript-api 生成 TypeScript 类型输出类似的东西:

export enum Item {
  Category = "Category",
  Alias = "Alias",
  Subheader = "Subheader",
}
...
export interface TableViewResponse {
  type?: "DefaultCategory";
  uri?: string;
  level?: string;
  name?: string;
  /** @format int32 */
  id?: number;
  parentCategory?: ParentCategoryResponse;
  items?: Item[];
}

请注意,在 Flow 和 TypeScript 类型中,我们应该具有 Item 可能是的每个可能对象的形状的类型信息,但我们没有任何此类类型信息。

这是项目数组的具体示例:

const items: Array<Item> = [
  {
    type: "Alias",
    level: "3",
    name: "name",
    id: 42,
    uri: "uri"
  },
  {
    type: "Category",
    level: "2",
    name: "name",
    id: 45,
    uri: "uri"
  }
]

希望现在清楚为什么 export type Item = { type: string }; 不是正确的类型定义。

问题:

是否有不同的模式可以生成所需的 export type Item = ChildCategoryResponse | AliasCategoryResponse | SubheaderResponse 输出而不是当前的 export type Item = { type: string }; 输出?

根据我阅读 the OpenAPI specification 的理解,oneOftype 不应一起使用。它似乎没有明确说明,但规范中的示例使用其中之一,而不是两者。我假设 TypeScript 绑定生成器在看到 type"object" 时,只是根据 properties 构造类型并忽略 oneOf.

如果你想区分 type 属性 作为联合类型之间的歧义,你应该使用 discriminator 而不是 properties:

"Item": {
    "oneOf": [
        {
            "$ref": "#/components/schemas/ChildCategoryResponse"
        },
        {
            "$ref": "#/components/schemas/AliasCategoryResponse"
        },
        {
            "$ref": "#/components/schemas/SubheaderResponse"
        }
    ],
    "discriminator": {
        "propertyName": "type",
        "mapping": {
            "Category": {
                "$ref": "#/components/schemas/ChildCategoryResponse"
            },
            "Alias": {
                "$ref": "#/components/schemas/AliasCategoryResponse"
            },
            "Subheader": {
                "$ref": "#/components/schemas/SubheaderResponse"
            }
        }
    }
},