openapi-typescript
                                
                                
                                
                                    openapi-typescript copied to clipboard
                            
                            
                            
                        type: object and additionalProperties: type: array causing is not assignable to 'string' index type
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
- [ ] I’m willing to open a PR (see CONTRIBUTING.md)
 
Maybe this is just invalid?
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 excellent explanation!
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.
The issue is that
keycould 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 allowkeyto 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.
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
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.
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.
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.