Skip to content

fix(emitter-framework): emit extends Record<string, T> for compatible types, additionalProperties for incompatible types#9897

Draft
Copilot wants to merge 8 commits intomainfrom
copilot/fix-emit-additional-properties
Draft

fix(emitter-framework): emit extends Record<string, T> for compatible types, additionalProperties for incompatible types#9897
Copilot wants to merge 8 commits intomainfrom
copilot/fix-emit-additional-properties

Conversation

Copy link
Contributor

Copilot AI commented Mar 4, 2026

Fixes the TypeScript Emitter Framework to correctly represent TypeSpec models with additional properties using TypeScript-idiomatic syntax based on type compatibility.

Behavior

TypeSpec Model TypeScript Interface
model X extends Record<unknown> { id: string; } interface X extends Record<string, unknown> { id: string; }
model X { name: string; ...Record<string>; } interface X extends Record<string, string> { name: string; } ← all properties assignable to T
model X { id: int32; ...Record<string>; } interface X { id: number; additionalProperties?: Record<string, string>; } ← incompatible types
model X { id: int32; ...Record<string>; ...Record<float32>; } interface X { id: number; additionalProperties?: Record<string, string | number>; } ← multiple spreads

Changes Made

  • interface-declaration.tsx: getExtendsType uses extends Record<string, T> only when all named properties are assignable to T. InterfaceBody adds additionalProperties?: Record<string, T> when named properties are incompatible with the record indexer type (TypeScript index signature constraint).
  • json-model-additional-properties-transform.tsx: Added usesWrappedAdditionalProperties helper that walks the model inheritance chain to determine whether to use the flat (destructuring) or wrapper (input_.additionalProperties) serialization approach.
  • interface-declaration.test.tsx: Updated tests to cover compatible types (extends Record<string, T>), incompatible types (additionalProperties?: Record<string, T>), and multiple record spreads producing union additionalProperties.
  • Scenario snapshots (spread.md, model_additional_properties.md, discriminated_union_spread.md): Updated to reflect the new interface shapes and serializer behavior.
  • e2e tests (main.test.ts, spreads.test.ts): Compatible types (ExtendsUnknown*, SpreadString, SpreadFloat, SpreadModel, etc.) use flat structure; incompatible types (DifferentSpread*, MultipleSpread, SpreadRecordNonDiscriminatedUnion*) use additionalProperties wrapper.
Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug]: TypeScript Emitter Framework shouldn't place additionalProperties on record types</issue_title>
<issue_description>This feels like an emitter-specific choice for how to represent models which extend records to me. If I want to emit an interface that represents what this looks like on the wire, I... can't?

That aside, it also presently emits just a plain record as an object type with a single property "additionalProperties" which also seems wrong - something like tags: Record<string> should probably emit as tags: { [string]: string } or even tags: Record<string, string> since there are no named properties to contend with.</issue_description>

<agent_instructions>use Record<string, string> in place of the current, strange additionalProperties property in the definition</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@markcowl Currently emits:
{ additionalProperties: Record<string, string> }
```</body></comment_new>
</comments>

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

…tionalProperties for record models

Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service bot added the emitter-framework Issues for the emitter framework label Mar 4, 2026
Copilot AI changed the title [WIP] Fix TypeScript emitter for record types fix(emitter-framework): emit extends Record<string, T> instead of additionalProperties property for record models Mar 4, 2026
@markcowl
Copy link
Contributor

markcowl commented Mar 4, 2026

@copilot pull the latest from main and then Fix failing tests in http-client-js

Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot pull the latest from main and then Fix failing tests in http-client-js

Updated the http-client-js test snapshots to reflect the new extends Record<string, T> interface output. All tests pass now (commit 4742306).

@microsoft-github-policy-service microsoft-github-policy-service bot added the emitter:client:js Issue for the JS client emitter: @typespec/http-client-js label Mar 4, 2026
@markcowl
Copy link
Contributor

markcowl commented Mar 4, 2026

@copilot pull the latest changes from the main branch and run pnpm format to fix any format issues in changed files

@markcowl
Copy link
Contributor

markcowl commented Mar 4, 2026

@copilot also fix any issues in the e2e tests that are failing

Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot pull the latest changes from the main branch and run pnpm format to fix any format issues in changed files

Merged the latest changes from main and ran pnpm format to fix formatting in the changed files (commit 39eb57b). All tests still pass.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • telemetry.astro.build
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/typespec/typespec/website/node_modules/.bin/../astro/astro.js build node s/.b�� ../../website/src/content/docs/d--output-dir sh tools/pnpm/10.30.2/bin/node --no-emit node bin/node node tobu�� rs &amp;&amp; pnpm build:src tsconfig.build.json tools/pnpm/10.30.2_tmp_4033/node_modules/pnpm/dist/node-gyp-bin/node ld.json &amp;&amp; pnpm sh node tobuf/reference node (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI and others added 2 commits March 4, 2026 22:11
…ectly instead of wrapping in additionalProperties

Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com>
…s skipping extends Record

Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot also fix any issues in the e2e tests that are failing

Fixed the e2e test failures in commits 9b066b1 and 789af83.

The root cause was that while the interface declarations were updated to use extends Record<string, T>, the JSON serializer transform (json-model-additional-properties-transform.tsx) was still wrapping additional properties in an additionalProperties key, causing type mismatches.

Changes made:

  • json-model-additional-properties-transform.tsx: Both transport and application transforms now spread additional properties directly onto the object (using destructuring to extract known props and spreading the rest), matching the extends Record<string, T> interface shape.
  • interface-declaration.tsx: Added a compatibility check — extends Record<string, T> is only emitted when all known properties are assignable to T, avoiding TypeScript index signature conflicts for "DifferentSpread" types (e.g., id: number with Record<string, string>).
  • e2e tests (main.test.ts, spreads.test.ts): Updated to use flat structure instead of additionalProperties wrapper.
  • Snapshot tests: Updated extends.md, spread.md, model_additional_properties.md, discriminated_union_spread.md to reflect the new serializer output.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • telemetry.astro.build
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/typespec/typespec/website/node_modules/.bin/../astro/astro.js build node /hom�� ebsite/src/conte--llmstxt node _modules/.bin/no../../website/src/content/docs/docs/emitters/openapi3/reference perimental gen-enode 7b6e072b8b5df199/home/REDACTED/work/typespec/typespec/packages/http-specs/node_modules�� st/node-gyp-bin/generate-scenarios-summary sh tobu�� sc -p ./tsconfig.build.json dotnet ode --no-emit ture tobuf/reference sh (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Contributor

@markcowl markcowl left a comment

Choose a reason for hiding this comment

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

@copilot also please:

  • pull the latest from main
  • ensure that all tests are passing and correct any issues
  • Add additional tests to cover multiple record spreads:
    model Foo { id: int32; ...Record<string>; ...Record<float32>} which should result in additionalProperties that are of type Record <string, string | float32>
  • ensure that all files use correct formatting (pnpm format)

}`);
});

it("omits extends clause when known properties are incompatible with the record element type", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

In this case, you should revert to using additionalProperties again

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted to additionalProperties for the incompatible case in commit c0acb37. When named properties are not assignable to the record element type, the interface now uses additionalProperties?: Record<string, T> instead of omitting the extends clause. Compatible types still use extends Record<string, T>.

…ord types, use extends Record for compatible types

- InterfaceBody now adds additionalProperties?: Record<string, T> when named properties are incompatible with the record element type (TypeScript index signature constraint)
- getExtendsType already skips extends clause for incompatible cases
- json-model-additional-properties-transform uses wrapper approach (input_.additionalProperties) for incompatible types and flat approach (destructuring) for compatible types
- Added multiple record spreads test (model Foo { id: int32; ...Record<string>; ...Record<float32> }) showing union additionalProperties
- Updated test snapshots to reflect new behavior
- Updated e2e tests to use additionalProperties wrapper for DifferentSpread/MultipleSpread types

Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot also please:

  • pull the latest from main
    ...

Done in commit c0acb37:

  • All tests updated and passing
  • Added test for multiple record spreads: model Foo { id: int32; ...Record<string>; ...Record<float32>; } which produces additionalProperties?: Record<string, string | number> (since int32 is not assignable to string | float32 in TypeSpec's type system, the wrapper approach is used)
  • Updated e2e tests for all DifferentSpread/MultipleSpread/NonDiscriminatedUnion types to use additionalProperties wrapper

Copilot AI changed the title fix(emitter-framework): emit extends Record<string, T> instead of additionalProperties property for record models fix(emitter-framework): emit extends Record<string, T> for compatible types, additionalProperties for incompatible types Mar 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:js Issue for the JS client emitter: @typespec/http-client-js emitter-framework Issues for the emitter framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: TypeScript Emitter Framework shouldn't place additionalProperties on record types

2 participants