Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-enum-style-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"swagger-typescript-api": minor
---

Add `enumStyle` option ("enum" | "union" | "const") to control enum output format. `"const"` generates `as const` objects with a companion type alias, including the built-in `ContentType` in the http-client. `generateUnionEnums` is deprecated in favor of `enumStyle: "union"`.
10 changes: 9 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,16 @@ const generateCommand = defineCommand({
},
"generate-union-enums": {
type: "boolean",
description: 'generate all "enum" types as union types (T1 | T2 | TN)',
description:
'(deprecated) generate all "enum" types as union types — use --enum-style=union instead',
default: codeGenBaseConfig.generateUnionEnums,
},
"enum-style": {
type: "string",
description:
'enum output style: "enum" (default), "union" (T1 | T2 | TN), or "const" (as const object + type alias)',
default: codeGenBaseConfig.enumStyle,
},
"http-client": {
type: "string",
description: `http client type (possible values: ${Object.values(
Expand Down Expand Up @@ -323,6 +330,7 @@ const generateCommand = defineCommand({
generateResponses: args.responses,
generateRouteTypes: args["route-types"],
generateUnionEnums: args["generate-union-enums"],
enumStyle: args["enum-style"] as "enum" | "union" | "const" | undefined,
httpClientType:
args["http-client"] || args.axios
? HTTP_CLIENT.AXIOS
Expand Down
14 changes: 13 additions & 1 deletion src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { consola } from "consola";
import { compact, merge, uniq } from "es-toolkit";
import type { OpenAPI } from "openapi-types";
import * as typescript from "typescript";
Expand Down Expand Up @@ -30,6 +31,7 @@ const TsKeyword = {
Date: "Date",
Type: "type",
Enum: "enum",
Const: "const",
Interface: "interface",
Array: "Array",
Record: "Record",
Expand All @@ -53,7 +55,9 @@ export class CodeGenConfig {
generateRouteTypes = false;
/** CLI flag */
generateClient = true;
/** CLI flag */
/** CLI flag. Controls enum output format: "enum" (default), "union" (T1 | T2 | TN), or "const" (as const object + type alias). */
enumStyle: "enum" | "union" | "const" = "enum";
/** @deprecated Use enumStyle: "union" instead */
generateUnionEnums = false;
/** CLI flag */
addReadonly = false;
Expand Down Expand Up @@ -460,5 +464,13 @@ export class CodeGenConfig {
if (this.enumNamesAsValues) {
this.extractEnums = true;
}
if (this.generateUnionEnums) {
consola.warn(
'`generateUnionEnums` is deprecated. Use `enumStyle: "union"` instead.',
);
if (this.enumStyle === "enum") {
this.enumStyle = "union";
}
}
};
}
5 changes: 4 additions & 1 deletion src/schema-parser/base-schema-parsers/discriminator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ export class DiscriminatorSchemaParser extends MonoSchemaParser {
]);
for (const [key, index] of enumEntries) {
const enumContent = parsedEnum.content?.[index];
if (this.config.generateUnionEnums) {
if (
this.config.enumStyle === "union" ||
this.config.enumStyle === "const"
) {
const literalValue =
enumContent?.value ??
(key !== undefined ? ts.StringValue(key) : undefined);
Expand Down
9 changes: 6 additions & 3 deletions src/schema-parser/base-schema-parsers/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,12 @@ export class EnumSchemaParser extends MonoSchemaParser {
schemaType: SCHEMA_TYPES.ENUM,
type: SCHEMA_TYPES.ENUM,
keyType: keyType,
typeIdentifier: this.config.generateUnionEnums
? this.config.Ts.Keyword.Type
: this.config.Ts.Keyword.Enum,
typeIdentifier:
this.config.enumStyle === "const"
? this.config.Ts.Keyword.Const
: this.config.enumStyle === "union"
? this.config.Ts.Keyword.Type
: this.config.Ts.Keyword.Enum,
name: this.typeName,
description: this.schemaFormatters.formatDescription(
this.schema.description,
Expand Down
12 changes: 11 additions & 1 deletion src/schema-parser/schema-formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@ export class SchemaFormatters {

base = {
[SCHEMA_TYPES.ENUM]: (parsedSchema) => {
if (this.config.generateUnionEnums) {
if (this.config.enumStyle === "const") {
return {
...parsedSchema,
$content: parsedSchema.content,
content: parsedSchema.content
.map(({ key, value }) => ` ${key}: ${value}`)
.join(",\n"),
};
}

if (this.config.enumStyle === "union") {
return {
...parsedSchema,
$content: parsedSchema.content,
Expand Down
10 changes: 10 additions & 0 deletions templates/base/data-contracts.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ const dataContractTemplates = {
type: (contract) => {
return `type ${contract.name}${buildGenerics(contract)} = ${contract.content}`;
},
const: (contract) => {
const entries = contract.$content
.map(({ key, value }) => ` ${key}: ${value}`)
.join(',\n');
const typeExport = contract.internal ? '' : 'export ';
return (
`const ${contract.name} = {\n${entries},\n} as const\n` +
`${typeExport}type ${contract.name} = (typeof ${contract.name})[keyof typeof ${contract.name}]`
);
},
}
%>

Expand Down
7 changes: 6 additions & 1 deletion templates/base/enum-data-contract.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ const { contract, utils, config } = it;
const { formatDescription, require, _ } = utils;
const { name, $content } = contract;
%>
<% if (config.generateUnionEnums) { %>
<% if (config.enumStyle === "const") { %>
export const <%~ name %> = {
<%~ _.map($content, ({ key, value }) => `${key}: ${value}`).join(",\n ") %>
} as const;
export type <%~ name %> = (typeof <%~ name %>)[keyof typeof <%~ name %>];
<% } else if (config.enumStyle === "union") { %>
export type <%~ name %> = <%~ _.map($content, ({ value }) => value).join(" | ") %>
<% } else { %>
export enum <%~ name %> {
Expand Down
11 changes: 11 additions & 0 deletions templates/base/http-clients/axios-http-client.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,24 @@ export interface ApiConfig<SecurityDataType = unknown> extends Omit<AxiosRequest
format?: ResponseType;
}

<% if (config.enumStyle === "const") { %>
export const ContentType = {
Json: "application/json",
JsonApi: "application/vnd.api+json",
FormData: "multipart/form-data",
UrlEncoded: "application/x-www-form-urlencoded",
Text: "text/plain",
} as const;
export type ContentType = (typeof ContentType)[keyof typeof ContentType];
<% } else { %>
export enum ContentType {
Json = "application/json",
JsonApi = "application/vnd.api+json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
<% } %>

export class HttpClient<SecurityDataType = unknown> {
public instance: AxiosInstance;
Expand Down
11 changes: 11 additions & 0 deletions templates/base/http-clients/fetch-http-client.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,24 @@ export interface HttpResponse<D extends unknown, E extends unknown = unknown> ex

type CancelToken = Symbol | string | number;

<% if (config.enumStyle === "const") { %>
export const ContentType = {
Json: "application/json",
JsonApi: "application/vnd.api+json",
FormData: "multipart/form-data",
UrlEncoded: "application/x-www-form-urlencoded",
Text: "text/plain",
} as const;
export type ContentType = (typeof ContentType)[keyof typeof ContentType];
<% } else { %>
export enum ContentType {
Json = "application/json",
JsonApi = "application/vnd.api+json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
<% } %>

export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = "<%~ apiConfig.baseUrl %>";
Expand Down
Loading