openapi-ts / openapi-typescript

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

[Feature request] Generate Enums AND Union types #941

Open mkosir opened 1 year ago

mkosir commented 1 year ago

Hey 👋 I think it would be great if we could generate Enum types and Union types all together (or at least as enum types). Basically same thing as Prisma does when generating types for database models 🚀 Wdyt?

drwpow commented 1 year ago

I think this could be offered as an option. I feel strongly that the default behavior of string unions shouldn’t change, because when data’s coming from an object it’s much easier to coerce a string union than a TypeScript enum.

One question I’d like to ask is: how would you envision this working? Like, what would the import path be? The following would have to be considered:

Adding a proposal that takes into account those things would really help implementing (asking for anyone reading this thread!)

mkosir commented 1 year ago

I think this could be offered as an option. I feel strongly that the default behaviour of string unions shouldn’t change, because when data’s coming from an object it’s much easier to coerce a string union than a TypeScript enum.

Absolutely agree on this đź’Ż . I also always go with string unions instead of enums. But there are rare occasions where one "have to" use enum, usually that happens when we want to iterate over the values and render them on frontend (dropdown selection, table etc.) and of course we don't want to duplicate and hardcode those values on frontend, since we will need to keep them up to date all the time with backend (which is the source of truth). Example: Role string union type defined in swagger, where on user creation page, we select predefined user role.

I wouldn't go with an option, but instead as you mentioned default behaviour of string unions shouldn't change, lets just add enums/object exported along with it.

If we check how Prisma does it: If database model User and Role are defined as

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  role      Role     @default(STANDARD)
}

enum Role {
  STANDARD
  APPRENTICE
  SUPERVISOR
  ADMINISTRATOR
}

Prisma CLI is going to generate types as:

export const Role: {
  STANDARD: 'STANDARD',
  APPRENTICE: 'APPRENTICE',
  SUPERVISOR: 'SUPERVISOR',
  ADMINISTRATOR: 'ADMINISTRATOR'
};

export type Role = (typeof Role)[keyof typeof Role]

And frontend can consume it as:

import { Role } from '@prisma/client';

type MyRoleType = Role; // MyRoleType is of type string union "APPRENTICE" | "STANDARD" | "SUPERVISOR" | "ADMINISTRATOR"
const myRole = Role.ADMINISTRATOR; // myRole value is "ADMINISTRATOR"

In our case of generating types from OpenAPI, I would maybe go with more explicit version, where beside generated string union Role (as it is already implemented now), there would be another "type"/enum generate alongside, with postfix Enum as RoleEnum. It would be collocated with string union type (components, paths, responses...). Wdyt?

duncanbeevers commented 1 year ago

Rather than generating an enum, how about generating a concrete array of strings, and then generating the string union from that?

export const RoleStrings = [
  'STANDARD',
  'APPRENTICE',
  'SUPERVISOR',
  'ADMINISTRATOR',
] as const;

export type Roles = typeof RoleStrings[number];

This way we don't have to worry about the weird behavior of keyof enum (which ends up containing Symbol and number)

mkosir commented 1 year ago

Also works, just accessing of the values becomes bit less idiomatic imo, Role[0] instead of Role.STANDARD

mkosir commented 1 year ago

Maybe approach form similar library will be helpful to implement this feature.

anthonyhastings commented 1 year ago

This is a very attractive feature for the same reasons that @mkosir outlined above. My use case would be that I've form controls in the UI where the values are dictated by an API and it's documentation. Right now i'm having to manually keep those choices in sync, but an enum would give me more flexibility to reference things. I'd definitely agree that any enums generated should be "extras" and not override string unions đź‘Ť

duncanbeevers commented 1 year ago

We've moved to serializing the openapi-typescript types and the JSON schema itself together in a single output file. This approach may serve your needs as well.

Doing so allows us to access the literal enum values, rather than just the types + unions, which is very useful both for getting access to schema union types, and for reflecting parts of the schema back to clients at run-time.

We started out using JSON module imports, but the the types from such imports are looser than we would like. Using as const gives us access to

import mySchema1 from 'schema.json';

const mySchema2 = {
  components: {
    schemas: {
      Status: {
        type: 'string',
        enum: ['initial', 'processing', 'complete']
      }
    }
} as const;

mySchema1.components.schemas.Status.enum; // string[]
mySchema2.components.schemas.Status.enum; // ['initial', 'processing', 'complete']

See this comment for more details.

djMax commented 1 year ago

I wrote this: https://github.com/openapi-typescript-infra/openapi-typescript-enum which will add enums. I don't like the way I had to write it - in that wrapping the CLI with a custom transform involves copying the CLI. Would be nice to have transform/postTransform be a CLI arg that points to some node-resolvable code.

drwpow commented 11 months ago

So I’ve already started planning some big changes to v7 (https://github.com/drwpow/openapi-typescript/discussions/1344) and I think that would make the enum work significantly easier.

The major blocker with enums is unlike unions they can’t be dynamically inlined in an interface. So that means hoisting out every enum in the spec, making sure it has a unique name that doesn’t conflict, and every single reference (even nested and deep references) is wired properly (across all files for multi-file schemas), is a decent chunk of work. Not impossible; we do that in other ways. But it’s just one more layer that’s not a quick change.

But if people aren’t opposed to some minor breaking changes, if we can lean on @redocly/openapi-core (which recently hit 1.0) for schema loading/parsing/bundling, then that greatly simplifies the work. So based on how that investigation goes, this may be a v7 feature.

This will get shipped either way, but between deep param scanning, discriminators, operations, and now enums, there are a lot of individual, overlapping efforts of “collect all the things, generate code, then reference everything properly” which Redocly could simplify greatly.

djMax commented 11 months ago

I'd definitely vote for centralizing on @redocly/openapi-core. I don't know how the community is or how the code is, but centralization in interpretation of openapi specs is undoubtedly a good thing given the intricacy of the spec and the changes that are likely to come along...

drwpow commented 9 months ago

Forgot to update this issue: the --enum flag exists in 7.x and is opt-in. This was just too complex to ship in 6.x.

7.x is still in testing and in the final bug-bashing phase, but is usable for most schemas today and will get a release candidate soon! And ICYMI it does rely on the Redocly CLI underneath for schema validation and parsing (which is lightweight and doesn’t introduce any bloat to the project).

djMax commented 9 months ago

Huzzah! Looking forward to adopting 7.x. We used redocly for our tooling too, so I'm glad to have picked the same. We're able to really slice and dice the specs this way. Now if only I could replace the Java codegen with something less horrific.

crutch12 commented 7 months ago

@drwpow current --enum behavior works... bad

In most cases I get such result:

/**
 * @description Status of deal
 * @enum {string}
 */
DealStatus: string; // wtf? why string?

Here is my scheme

"DealStatus": {
  "type": "string",
  "additionalProperties": false,
  "description": "Status",
  "enum": [
    "UNDEFINED",
    "CURRENT",
    "OBSERVATION",
    "POTENT_PROBLEM",
    "PROBLEM",
    "EMPTY"
  ]
},

Tried with openapi-typescript@7.0.0-next.5

iffa commented 5 months ago

It's nice that v7 has tne enum flag, but I'd love to see this issue resolved (generating both)

github-actions[bot] commented 1 month ago

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.