Skip to content

Commit 96267de

Browse files
authored
Refactor type-utils.ts for readability and safety (#72)
- Remove unused UnionStatementNode import - Rename shadowed `split` parameter to `splitFn` in splitWithAcronyms - Refactor getUnionName from switch(true) to if/else with early returns, removing non-null assertions by using return values as guards - Fix getUnionNameForOperation: remove incorrect UnionStatementNode cast, add null safety for missing operationNode - Refactor isTrueModel from switch(true) with fallthrough to simple if/return guards
1 parent 7276d90 commit 96267de

1 file changed

Lines changed: 54 additions & 61 deletions

File tree

packages/graphql/src/lib/type-utils.ts

Lines changed: 54 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
type ModelStatementNode,
2525
type Node,
2626
SyntaxKind,
27-
type UnionStatementNode,
2827
} from "@typespec/compiler/ast";
2928
import { camelCase, constantCase, pascalCase, split, splitSeparateNumbers } from "change-case";
3029
import { reportDiagnostic } from "../lib.js";
@@ -74,16 +73,18 @@ export function getTemplatedModelName(model: Model): string {
7473
}
7574

7675
function splitWithAcronyms(
77-
split: (name: string) => string[],
76+
splitFn: (name: string) => string[],
7877
skipStart: boolean,
7978
name: string,
8079
): string[] {
81-
const parts = split(name);
80+
const parts = splitFn(name);
8281

8382
if (name === name.toUpperCase()) {
8483
return parts;
8584
}
86-
// Preserve strings of capital letters, e.g. "API" should be treated as three words ["A", "P", "I"] instead of one word
85+
// Split consecutive capital letters into individual characters for proper casing,
86+
// e.g. "API" becomes ["A", "P", "I"] so PascalCase produces "Api" → but we preserve
87+
// all-caps names at the toTypeName level, so this only affects mixed-case like "APIResponse".
8788
return parts.flatMap((part, index) => {
8889
if (skipStart && index === 0) return part;
8990
if (part.match(/^[A-Z]+$/)) return part.split("");
@@ -140,45 +141,43 @@ function getNameWithoutNamespace(name: string): string {
140141

141142
/** Generate a GraphQL type name for a union, including anonymous unions. */
142143
export function getUnionName(union: Union, program: Program): string {
143-
// SyntaxKind.UnionExpression: Foo | Bar
144-
// SyntaxKind.UnionStatement: union FooBarUnion { Foo, Bar }
145-
// SyntaxKind.TypeReference: FooBarUnion
146-
147-
const templateString = getTemplateString(union) ? "Of" + getTemplateString(union) : "";
148-
149-
switch (true) {
150-
case !!union.name:
151-
// The union is not anonymous, use its name
152-
return union.name;
153-
154-
case isReturnType(union):
155-
// The union is a return type, use the name of the operation
156-
// e.g. op getBaz(): Foo | Bar => GetBazUnion
157-
return `${getUnionNameForOperation(program, union)}${templateString}Union`;
158-
159-
case isModelProperty(union):
160-
// The union is a model property, name it based on the model + property
161-
// e.g. model Foo { bar: Bar | Baz } => FooBarUnion
162-
const modelProperty = getModelProperty(union);
163-
const propName = toTypeName(getNameForNode(modelProperty!));
164-
const unionModel = union.node?.parent?.parent as ModelStatementNode;
165-
const modelName = unionModel ? getNameForNode(unionModel) : "";
166-
return `${modelName}${propName}${templateString}Union`;
167-
168-
case isAliased(union):
169-
// The union is an alias, name it based on the alias name
170-
// e.g. alias Baz = Foo<string> | Bar => Baz
171-
const alias = getAlias(union);
172-
const aliasName = getNameForNode(alias!);
173-
return `${aliasName}${templateString}`;
174-
175-
default:
176-
reportDiagnostic(program, {
177-
code: "unrecognized-union",
178-
target: union,
179-
});
180-
return "UnknownUnion";
144+
// Named union — use its name directly
145+
if (union.name) {
146+
return union.name;
181147
}
148+
149+
const ts = getTemplateString(union);
150+
const templateString = ts ? "Of" + ts : "";
151+
152+
// Anonymous return type — name after the operation
153+
// e.g. op getBaz(): Foo | Bar => GetBazUnion
154+
if (isReturnType(union)) {
155+
return `${getUnionNameForOperation(program, union)}${templateString}Union`;
156+
}
157+
158+
// Anonymous model property — name after model + property
159+
// e.g. model Foo { bar: Bar | Baz } => FooBarUnion
160+
const modelProperty = getModelProperty(union);
161+
if (modelProperty) {
162+
const propName = toTypeName(getNameForNode(modelProperty));
163+
const unionModel = union.node?.parent?.parent as ModelStatementNode;
164+
const modelName = unionModel ? getNameForNode(unionModel) : "";
165+
return `${modelName}${propName}${templateString}Union`;
166+
}
167+
168+
// Alias — name after the alias
169+
// e.g. alias Baz = Foo<string> | Bar => Baz
170+
const alias = getAlias(union);
171+
if (alias) {
172+
const aliasName = getNameForNode(alias);
173+
return `${aliasName}${templateString}`;
174+
}
175+
176+
reportDiagnostic(program, {
177+
code: "unrecognized-union",
178+
target: union,
179+
});
180+
return "UnknownUnion";
182181
}
183182

184183
function isNamedType(type: Type | Value | IndeterminateEntity): type is { name: string } & Type {
@@ -216,8 +215,9 @@ function getNameForNode(node: NamedNode): string {
216215
}
217216

218217
function getUnionNameForOperation(program: Program, union: Union): string {
219-
const operationNode = (union.node as UnionStatementNode).parent?.parent;
220-
const operation = program.checker.getTypeForNode(operationNode!);
218+
const operationNode = union.node?.parent?.parent;
219+
if (!operationNode) return "Unknown";
220+
const operation = program.checker.getTypeForNode(operationNode);
221221

222222
return toTypeName(getTypeName(operation));
223223
}
@@ -303,26 +303,19 @@ function getTemplateStringInternal(
303303
args: string[],
304304
options: { conjunction: string } = { conjunction: "And" },
305305
): string {
306+
// Apply toTypeName to convert raw compiler names (e.g., "string") to GraphQL PascalCase ("String")
306307
return args.length > 0 ? args.map(toTypeName).join(options.conjunction) : "";
307308
}
308309

309310
/** Check if a model should be emitted as a GraphQL object type (not an array, record, or never). */
310311
export function isTrueModel(model: Model): boolean {
311-
/* eslint-disable no-fallthrough */
312-
switch (true) {
313-
// A scalar array is represented as a model with an indexer
314-
// and a scalar type. We don't want to emit this as a model.
315-
case isScalarOrEnumArray(model):
316-
// A union array is represented as a model with an indexer
317-
// and a union type. We don't want to emit this as a model.
318-
case isUnionArray(model):
319-
case isNeverType(model):
320-
// If the model is purely a record, we don't want to emit it as a model.
321-
// Instead, we will need to create a scalar
322-
case isRecordType(model) && [...walkPropertiesInherited(model)].length === 0:
323-
return false;
324-
default:
325-
return true;
326-
}
327-
/* eslint-enable no-fallthrough */
312+
return !(
313+
// Array of scalars/enums — represented as a list type, not an object type
314+
isScalarOrEnumArray(model) ||
315+
// Array of unions — represented as a list type, not an object type
316+
isUnionArray(model) ||
317+
isNeverType(model) ||
318+
// Pure record with no properties — emitted as a custom scalar, not an object type
319+
(isRecordType(model) && [...walkPropertiesInherited(model)].length === 0)
320+
);
328321
}

0 commit comments

Comments
 (0)