Skip to content

Commit 57ebc4f

Browse files
committed
fix(typescript): Handle invalid type names
1 parent 5028056 commit 57ebc4f

File tree

2 files changed

+80
-25
lines changed

2 files changed

+80
-25
lines changed

plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,30 @@ describe("schemaToTypeAliasDeclaration", () => {
946946
`);
947947
});
948948

949+
it("should generate valid identifier from number name", () => {
950+
const schema: SchemaObject = {};
951+
952+
expect(printSchema(schema, "200")).toMatchInlineSnapshot(`
953+
"export type TwoHundred = void;"
954+
`);
955+
});
956+
957+
it("should generate valid identifier from symbol name", () => {
958+
const schema: SchemaObject = {};
959+
960+
expect(printSchema(schema, "-")).toMatchInlineSnapshot(`
961+
"export type _ = void;"
962+
`);
963+
});
964+
965+
it("should generate valid identifier from invalid name", () => {
966+
const schema: SchemaObject = {};
967+
968+
expect(printSchema(schema, "🙂")).toMatchInlineSnapshot(`
969+
"export type _ = void;"
970+
`);
971+
});
972+
949973
it("should generate a `never` if the combined type is broken", () => {
950974
const schema: SchemaObject = {
951975
allOf: [{ type: "string" }, { type: "number" }],

plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@ import {
1111
} from "openapi3-ts/oas30";
1212
import { singular } from "pluralize";
1313
import { isValidIdentifier } from "tsutils";
14-
import ts, { factory as f } from "typescript";
14+
import ts, {
15+
factory as f,
16+
isIdentifierPart,
17+
isIdentifierStart,
18+
} from "typescript";
19+
import { convertNumberToWord } from "../utils/getEnumProperties";
1520
import { getReferenceSchema } from "./getReference";
1621

1722
type RemoveIndex<T> = {
1823
[P in keyof T as string extends P
19-
? never
20-
: number extends P
21-
? never
22-
: P]: T[P];
24+
? never
25+
: number extends P
26+
? never
27+
: P]: T[P];
2328
};
2429

2530
export type OpenAPIComponentType = Extract<
@@ -56,11 +61,37 @@ export const schemaToTypeAliasDeclaration = (
5661
const jsDocNode = isSchemaObject(schema)
5762
? getJSDocComment(schema, context)
5863
: undefined;
64+
65+
let identifier = pascal(name);
66+
67+
if (identifier.length > 0 && !isNaN(Number(identifier))) {
68+
// If the identifier can be cast to a number, convert it to a word.
69+
identifier = pascal(convertNumberToWord(Number(identifier)));
70+
} else {
71+
// If the identifier does not start with a valid character but valid identifier part, prefix it with an underscore.
72+
if (
73+
!isIdentifierStart(identifier.charCodeAt(0), ts.ScriptTarget.Latest) &&
74+
isIdentifierPart(identifier.charCodeAt(0), ts.ScriptTarget.Latest)
75+
) {
76+
identifier = `_${identifier}`;
77+
}
78+
79+
// If the identifier is still not valid, remove invalid characters.
80+
if (!isValidIdentifier(identifier)) {
81+
identifier = identifier.replace(/[^a-zA-Z0-9_]/g, "");
82+
}
83+
84+
// If the identifier is now empty, set it to "_".
85+
if (identifier.length === 0) {
86+
identifier = "_";
87+
}
88+
}
89+
5990
const declarationNode = f.createTypeAliasDeclaration(
6091
[f.createModifier(ts.SyntaxKind.ExportKeyword)],
61-
pascal(name),
92+
identifier,
6293
undefined,
63-
getType(schema, context, name)
94+
getType(schema, context, identifier)
6495
);
6596

6697
return jsDocNode ? [jsDocNode, declarationNode] : [declarationNode];
@@ -89,11 +120,11 @@ export const getType = (
89120

90121
let refNode: ts.TypeNode = f.createTypeReferenceNode(
91122
namespace === context.currentComponent
92-
? f.createIdentifier(pascal(name))
123+
? f.createIdentifier(name)
93124
: f.createQualifiedName(
94-
f.createIdentifier(pascal(namespace)),
95-
f.createIdentifier(pascal(name))
96-
)
125+
f.createIdentifier(pascal(namespace)),
126+
f.createIdentifier(name)
127+
)
97128
);
98129

99130
for (let i = 0; i < propertyPath.length; i++) {
@@ -173,7 +204,7 @@ export const getType = (
173204

174205
if (schema.enum) {
175206
if (isNodeEnum) {
176-
return f.createTypeReferenceNode(f.createIdentifier(pascal(name || "")));
207+
return f.createTypeReferenceNode(f.createIdentifier(name || ""));
177208
}
178209

179210
const unionTypes = f.createUnionTypeNode([
@@ -287,9 +318,9 @@ export const getType = (
287318
return withNullable(
288319
members.length > 0
289320
? f.createIntersectionTypeNode([
290-
f.createTypeLiteralNode(members),
291-
f.createTypeLiteralNode([additionalPropertiesNode]),
292-
])
321+
f.createTypeLiteralNode(members),
322+
f.createTypeLiteralNode([additionalPropertiesNode]),
323+
])
293324
: f.createTypeLiteralNode([additionalPropertiesNode]),
294325
schema.nullable
295326
);
@@ -618,16 +649,16 @@ export const getJSDocComment = (
618649
// `allOf` can add some documentation to the schema, let’s merge all items as first step
619650
const schemaWithAllOfResolved = schema.allOf
620651
? schema.allOf.reduce<SchemaObject>((mem, allOfItem) => {
621-
if (isReferenceObject(allOfItem)) {
622-
const referenceSchema = getReferenceSchema(
623-
allOfItem.$ref,
624-
context.openAPIDocument.components
625-
);
626-
return mergeSchemas(mem, referenceSchema).mergedSchema;
627-
} else {
628-
return mergeSchemas(mem, allOfItem).mergedSchema;
629-
}
630-
}, schema)
652+
if (isReferenceObject(allOfItem)) {
653+
const referenceSchema = getReferenceSchema(
654+
allOfItem.$ref,
655+
context.openAPIDocument.components
656+
);
657+
return mergeSchemas(mem, referenceSchema).mergedSchema;
658+
} else {
659+
return mergeSchemas(mem, allOfItem).mergedSchema;
660+
}
661+
}, schema)
631662
: schema;
632663

633664
const getJsDocIdentifier = (value: unknown) => {

0 commit comments

Comments
 (0)