Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature request] Generate Enums AND Union types #941

Open
mkosir opened this issue Sep 23, 2022 · 16 comments
Open

[Feature request] Generate Enums AND Union types #941

mkosir opened this issue Sep 23, 2022 · 16 comments
Labels
enhancement New feature or request PRs welcome PRs are welcome to solve this issue!

Comments

@mkosir
Copy link

mkosir commented Sep 23, 2022

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 drwpow added enhancement New feature or request PRs welcome PRs are welcome to solve this issue! labels Sep 23, 2022
@drwpow
Copy link
Contributor

drwpow commented Sep 23, 2022

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:

  • Enums could come from components as well as paths and responses
  • Account for deep-linking of enums (e.g. components['schemas']['TopLevel']['NestedOne']['NestedTwo']['EnumProperty'])
  • Also account for invalid TypeScript characters ("Enum Type #1")
  • Lastly, whatever gets exported can’t have any conflicts with any other part of their schema (e.g. it would be impossible for them to add any property that conflicts)

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

@mkosir
Copy link
Author

mkosir commented Sep 25, 2022

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
Copy link
Contributor

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
Copy link
Author

mkosir commented Feb 28, 2023

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

@mkosir
Copy link
Author

mkosir commented Jun 6, 2023

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

@anthonyhastings
Copy link

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
Copy link
Contributor

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
Copy link

djMax commented Jul 28, 2023

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
Copy link
Contributor

drwpow commented Sep 20, 2023

So I’ve already started planning some big changes to v7 (#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
Copy link

djMax commented Sep 20, 2023

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...

This was referenced Oct 4, 2023
@drwpow
Copy link
Contributor

drwpow commented Dec 15, 2023

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
Copy link

djMax commented Dec 15, 2023

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
Copy link
Contributor

@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
Copy link

iffa commented Apr 12, 2024

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

Copy link
Contributor

github-actions bot commented Aug 6, 2024

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.

@darkbasic
Copy link

Having this would make --enum-values redundant: #1616 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request PRs welcome PRs are welcome to solve this issue!
Projects
None yet
Development

No branches or pull requests

8 participants