openapi-ts / openapi-typescript

Generate TypeScript types from OpenAPI 3 specs
https://openapi-ts.dev
MIT License
5.43k stars 447 forks source link

Generates invalid types when API paths are nested #1752

Open mat-certn opened 1 month ago

mat-certn commented 1 month ago

Description

We have multiple paths in our schema that are "nested" below each other, like so:

/some/path/{id}
/some/path/{id}/summary

openapi-typescript using the argument --path-params-as-types generates the following:

  [path: `/some/path/${string}`]: SchemaOne;
  [path: `/some/path/${string}/summary`]: SchemaTwo;

which conflicts with the error

'x' index type 'SchemaOne' is not assignable to 'x' index type 'SchemaTwo'.

this is because technically /some/path/${string}/summary is generated as incorrectly assignable to /some/path/${string} - instead of ${string} it should be some sort of type that cannot include "/"

Name Version
openapi-typescript 7.0.2
TypeScript 5.3.3 or 5.5.3 (have tried a few versions)
Node.js 18.18.2
OS + version macOS 14.5

Reproduction

Generate types using the following schema:

openapi: 3.0.3
servers:
  - url: http://some-server.com
    description: Local development server
info:
  title: Example API with conflicting paths
  version: 0.0.1
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT
paths:
  /api/internal/cases/{short_id}:
    get:
      summary: "Get case details"
      operationId: cases_retrieve
      parameters:
        - in: path
          name: short_id
          schema:
            type: string
          required: true
      tags:
        - cases
      security:
        - AuthZeroUserAuth: []
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CaseDetail"
          description: ""
        "404":
          description: No response body
  /api/internal/cases/{short_id}/summary:
    get:
      summary: "Get case summary"
      operationId: cases_summary_retrieve
      parameters:
        - in: path
          name: short_id
          schema:
            type: string
          required: true
      tags:
        - cases
      security:
        - AuthZeroUserAuth: []
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CaseSummary"
          description: ""
        "404":
          description: No response body
components:
  schemas:
    CaseDetail:
      type: object
      properties:
        case_name:
          type: string
          format: uuid
          readOnly: true
      required:
        - case_name
    CaseSummary:
      type: object
      properties:
        case_summary:
          type: string
          format: uuid
          readOnly: true
      required:
        - case_summary
  securitySchemes:
    AuthZeroUserAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

npx openapi-typescript example.yml --output example.ts --path-params-as-types

Expected result

A more complex type than ${string} that prevents both endpoints from matching each other.

Checklist

validating example.yml... example.yml: validated in 12ms

Woohoo! Your API description is valid. 🎉


- [ ] I’m willing to open a PR (see [CONTRIBUTING.md](https://github.com/openapi-ts/openapi-typescript/blob/main/packages/openapi-typescript/CONTRIBUTING.md))
phk422 commented 1 month ago

Here's one scenario I can think of:

type BasePath = string & { __id: 'BasePath' };

type SchemaOne = {
  case_name: string;
};

type SchemaTwo = {
  case_summary: string;
};

type Paths = {
  [path: `/api/internal/cases/${BasePath}`]: SchemaOne;
  [path: `/api/internal/cases/${BasePath}/summary`]: SchemaTwo;
};

const caseDetail: Paths[`/api/internal/cases/${BasePath}`] = {
  case_name: 'example-case',
};

const caseSummary: Paths[`/api/internal/cases/${BasePath}/summary`] = {
  case_summary: 'example-summary',
};

@drwpow @mat-certn I want to know what you think?

mat-certn commented 1 month ago

i think for my use-case I want to find the type definition from the URL, e.g. I want to be able to do this:

const url = `/api/internal/cases/12345/summary`

const caseSummary: Paths[url] = {
  case_summary: 'example-summary',
};

which doesn't work with the above: /api/internal/cases/12345/summary does not exist on type Paths

phk422 commented 1 month ago

i think for my use-case I want to find the type definition from the URL, e.g. I want to be able to do this:

const url = `/api/internal/cases/12345/summary`

const caseSummary: Paths[url] = {
  case_summary: 'example-summary',
};

which doesn't work with the above: /api/internal/cases/12345/summary does not exist on type Paths

Are there any good solutions for this in ts?

drwpow commented 2 weeks ago

Yeah this is a tricky one! I will admit this flag has always had some rough edges for reasons like this. Loose pattern-matching in TS isn’t as robust as runtime equivalents. I’m sure there are ways to solve it, but they may not be straightforward. Either way, would welcome PRs on improving this.