Skip to content

Add TCGC exact-name support to TypeScript emitter - #5331

Open
Qiaoqiao Zhang (qiaozha) wants to merge 1 commit into
Azure:mainfrom
qiaozha:typespec-ts-exact-name
Open

Add TCGC exact-name support to TypeScript emitter#5331
Qiaoqiao Zhang (qiaozha) wants to merge 1 commit into
Azure:mainfrom
qiaozha:typespec-ts-exact-name

Conversation

@qiaozha

Copy link
Copy Markdown
Member

Summary

  • honor TCGC exact() names across TypeScript clients, operations, parameters, models, properties, and enum members
  • keep exact names consistent in serializers, classic clients, generated samples/tests, and client hierarchy paths
  • report deduplicated warnings at emission sites when an exact name cannot form a valid TypeScript identifier
  • add focused unit coverage and opt the emitter into the shared exact-name Spector scenario with a generated API baseline

Validation

  • mise exec -- pnpm format
  • mise exec -- pnpm lint
  • mise exec -- pnpm --filter @azure-tools/typespec-ts build
  • mise exec -- pnpm --dir packages/typespec-ts unit-test (669 tests)
  • mise exec -- pnpm --dir packages/typespec-ts test-next (254 tests)
  • exact-name Spector integration (5 tests)

@github-actions

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @azure-tools/typespec-ts
Show changes

@azure-tools/typespec-ts - feature ✏️

Honor TCGC exact() names without applying TypeScript casing transformations.

@github-actions

Copy link
Copy Markdown
Contributor

📦 Package size report

1 package changed size, +4.52 KB (+0.0%) packed overall.

Package Packed (base → head) Δ Packed Unpacked (base → head) Δ Unpacked
@azure-tools/typespec-ts 527.18 KB → 531.75 KB +4.57 KB (+0.9%) 🔴 2.54 MB → 2.56 MB +24.96 KB (+1.0%) 🔴
12 package(s) with no notable change
Package Packed (base → head) Δ Packed Unpacked (base → head) Δ Unpacked
@azure-tools/typespec-java 13.51 MB → 13.51 MB -49 B (-0.0%) 15.03 MB → 15.03 MB
@azure-tools/azure-http-specs 146.71 KB → 146.71 KB 1.16 MB → 1.16 MB
@azure-tools/typespec-autorest 80.93 KB → 80.93 KB 395.06 KB → 395.06 KB
@azure-tools/typespec-autorest-canonical 7.42 KB → 7.42 KB 26.00 KB → 26.00 KB
@azure-tools/typespec-azure-core 129.43 KB → 129.43 KB 702.77 KB → 702.77 KB
@azure-tools/typespec-azure-portal-core 42.40 KB → 42.40 KB 192.91 KB → 192.91 KB
@azure-tools/typespec-azure-resource-manager 171.92 KB → 171.92 KB 1.04 MB → 1.04 MB
@azure-tools/typespec-azure-rulesets 5.16 KB → 5.16 KB 32.09 KB → 32.09 KB
@azure-tools/typespec-client-generator-core 229.25 KB → 229.25 KB 1.23 MB → 1.23 MB
@azure-tools/typespec-go 259.93 KB → 259.93 KB 1.33 MB → 1.33 MB
@azure-tools/typespec-metadata 15.91 KB → 15.91 KB 62.26 KB → 62.26 KB
@azure-tools/typespec-python 42.22 KB → 42.22 KB 164.91 KB → 164.91 KB

Packed = gzipped .tgz published to npm. Unpacked = total extracted size. 🆕 added, 🗑️ removed. Packages from the core/ submodule are not included.
🔴 grew · 🟢 shrank — only changes of at least 512 B and 0.5% are marked.

@pkg-pr-new

pkg-pr-new Bot commented Aug 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@azure-tools/typespec-ts@5331

commit: 2c925b4

@azure-sdk-automation

Copy link
Copy Markdown
Contributor

You can try these changes here

🛝 Playground 🌐 Website

if (!isValidTypeScriptIdentifierName(name)) {
return false;
}
const result = ts.transpileModule(`export const ${name} = 0;`, {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this transpileModule more time-consuming than a normal regex? Since the normalizeModelName function that calls this function is called from many places, do we need to add a cache?

}

export function normalizeSdkPropertyName(sdkName: SdkName): string {
return sdkName.isExactName ? sdkName.name : normalizeName(sdkName.name, NameType.Property);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be return normalizeSdkName(sdkName, NameType.Property); to avoid duplicate implementation?

@@ -0,0 +1,285 @@
import { afterAll, describe, expect, it } from "vitest";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we move the tests to scenarios/ and use the Markdown pattern.

? "_"
: "";
return `${internalModelPrefix}${normalizeName(namespacePrefix + type.name, nameType, true)}${unionSuffix}`;
const normalizedNamespacePrefix =

@JialinHuang803 Jialin Huang (JialinHuang803) Aug 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what the expected behavior should be when enableModelNamespace is true. Do we need to add the namespace prefix for the models with exact name? But I believe this is a rare case, just want to confirm the behavior. Maybe we can add e unit test for it as well.

nameType,
{ shouldGuard: true },
);
return `${internalModelPrefix}${modelName}${unionSuffix}`;

@JialinHuang803 Jialin Huang (JialinHuang803) Aug 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be clearer if we separate namespace prefix from model name as now they have different normalize logic:

const namespacePrefix = context.emitterOptions?.enableModelNamespace
  ? normalizeName(segments.join(""), nameType)
  : "";

const modelName = normalizeSdkName(type, nameType, { shouldGuard: true }});

return `${internalModelPrefix}${namespacePrefix}${modelName}${unionSuffix}`;

const groupName = normalizeName(rawGroupName, NameType.Property);
const groupName = normalizeName(prefixes[0] ?? "", NameType.Property);
const existProperty = clientClass.getProperties().filter((p) => {
return p.getName() === normalizeName(groupName, NameType.Property);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's an issue with the existing code, not introduced by this PR - seems like we don't need to call normalizedName again because the groupName is already normalized.

I just tried with nested operation groups:

namespace Operations {
  @route("/direct")
  @get
  op directOperation(): void;

  namespace ChildOne {
    @route("/child-one")
    @get
    op childOneOperation(): void;
  }

  namespace ChildTwo {
    @route("/child-two")
    @get
    op childTwoOperation(): void;
  }
}

#suppress "experimental-feature" "exact name test"
@@clientName(Operations, exact("my_group"));

And the generated code has three my_group properties in the classic client.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:typescript Issues for @azure-tools/typespec-ts emitter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants