openapi-typescript icon indicating copy to clipboard operation
openapi-typescript copied to clipboard

type: object and additionalProperties: type: array causing is not assignable to 'string' index type

Open joshunger opened this issue 2 years ago • 7 comments

Description

I'm seeing in our existing code we have a type object where we've defined additional properties as an array. When we run openapi-typescript the TypeScript produced doesn't compile. Should this work? Thanks for the help!

TS2411: Property 'totals' of type '{ count?: number; }' is not assignable to 'string' index type 'Record<string, never>[]'.
    10 |     B: Record<string, never>;
    11 |     Example: {
  > 12 |       totals?: {
       |       ^^^^^^
    13 |         count?: number;
    14 |       };
    15 |       [key: string]: (components['schemas']['A'] & components['schemas']['B'])[] | undefined;
Name Version
openapi-typescript 6.2.0
Node.js 14.17.6
OS + version macOS 13

Reproduction

schema.yaml

components:
  schemas:
    A:
      type: object
    B:
      type: object
    Example:
      type: object
      properties:
        totals:
          type: object
          properties:
            count:
              type: integer
      additionalProperties:
        type: array
        items:
          allOf:
            - $ref: '#/components/schemas/A'
            - $ref: '#/components/schemas/B'
npx openapi-typescript ./schema.yaml

Expected result

Creates valid TypeScript that compiles.

Checklist

joshunger avatar Mar 22 '23 16:03 joshunger

Maybe this is just invalid?

joshunger avatar Mar 22 '23 17:03 joshunger

This is a tricky one.

Let's take a simplified schema:

Example:
  type: object
  properties:
    totals:
      type: number
  additionalProperties:
    type: string

v6 generates:

Example: {
  totals?: number;
  [key: string]: string | undefined;
};

while v5 generates:

Example: {
  totals?: number;
} & { [key: string]: string };

Note the difference - an intersection. While the v5 generated types compile, they're not actually useful, since trying to assign anything to that type will fail:

examples/1055_v5.ts:16:7 - error TS2322: Type '{ totals: number; }' is not assignable to type '{ totals: number; } & { [key: string]: string; }'.
  Type '{ totals: number; }' is not assignable to type '{ [key: string]: string; }'.
    Property 'totals' is incompatible with index signature.
      Type 'number' is not assignable to type 'string'.

const myExample: components["schemas"]["Example"] = {
  totals: 4
}

Following discussion from https://github.com/microsoft/TypeScript/issues/17867... there's a misunderstanding on what the index signature here actually means. If you were to index an object of this type with an arbitrary string, what would you expect? You would expect string, but if that string is totals, then you actually get number. See Ryan Cavanaugh's comment: https://github.com/microsoft/TypeScript/issues/17867#issuecomment-323176309

So, what we probably want to generate is actually:

Example: {
  totals?: number;
  [key: string]: string | undefined | number;
};

which works as on the tin. Index with an arbitrary string, and you get string, other than the case where the string happens to be totals, where it's number. So the type should be string | number.

Does that make sense? This is a genuine bug.

mitchell-merry avatar Apr 07 '23 01:04 mitchell-merry

@mitchell-merry excellent explanation!

joshunger avatar Apr 12 '23 22:04 joshunger

I'm experiencing this when trying to generate a wrapper for Creditsafe's API.

The issue is at line 21764 of their spec. Simplified, it's:

"ProblemDetails": {
  "type": "object",
  "properties": {
    "title": "string"
  },
  "additionalProperties": {
    "type": "object"
  }
}

The generator produces

ProblemDetails: {
  title?: string;
  [key: string]: Record<string, never> | undefined;
};

which is not valid TypeScript. But afaict this is valid OpenAPI.

Basically it's all or nothing. If you have additionalProperties, you can't have any other properties.

The issue is that key could clash at runtime with any of the other fields. We'd need to define an enum consisting of all the keys of that type, then allow key to be any string except that type. Not sure if this is possible.

tyronen9 avatar Jul 14 '23 18:07 tyronen9

The issue is that key could clash at runtime with any of the other fields. We'd need to define an enum consisting of all the keys of that type, then allow key to be any string except that type. Not sure if this is possible.

I agree this is probably the right fix. And it should be possible.

drwpow avatar Jul 14 '23 19:07 drwpow

I don't think it's currently possible to specify a type string except for a particular string. There is an issue open for negated types: https://github.com/microsoft/TypeScript/issues/4196 that would let us do something like [key: string & not 'title'] for the index signature. Would be a good use case for that operand I suppose

mitchell-merry avatar Jul 14 '23 19:07 mitchell-merry

Yeah I’m wondering if reverting to the old behavior of an intersection + a TS helper (or it sounds crazy, but maybe a union, depending on how it behaves in practice) would yield better results until https://github.com/microsoft/TypeScript/issues/4196 resolves (which may not be anytime soon). I think it’s at least worth exploring some (non-breaking) TS shenanigans here.

drwpow avatar Jul 14 '23 21:07 drwpow

This issue is stale because it has been open for 90 days with no activity. If there is no activity in the next 7 days, the issue will be closed.

github-actions[bot] avatar Aug 06 '24 12:08 github-actions[bot]

This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem.

github-actions[bot] avatar Aug 15 '24 02:08 github-actions[bot]