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

[Feature request] Generate Enums AND Union types

Open mkosir opened this issue 2 years ago • 15 comments

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?

mkosir avatar Sep 23 '22 07:09 mkosir

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!)

drwpow avatar Sep 23 '22 18:09 drwpow

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?

mkosir avatar Sep 25 '22 11:09 mkosir

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)

duncanbeevers avatar Feb 21 '23 08:02 duncanbeevers

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

mkosir avatar Feb 28 '23 08:02 mkosir

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

mkosir avatar Jun 06 '23 07:06 mkosir

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 👍

anthonyhastings avatar Jul 18 '23 08:07 anthonyhastings

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.

duncanbeevers avatar Jul 18 '23 17:07 duncanbeevers

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.

djMax avatar Jul 28 '23 22:07 djMax

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.

drwpow avatar Sep 20 '23 15:09 drwpow

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

djMax avatar Sep 20 '23 16:09 djMax

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

drwpow avatar Dec 15 '23 00:12 drwpow

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.

djMax avatar Dec 15 '23 00:12 djMax

@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 [email protected]

crutch12 avatar Jan 24 '24 14:01 crutch12

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

iffa avatar Apr 12 '24 07:04 iffa

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]