TypeSpec 1.10 Release Notes
TypeSpec 1.10.0 Release Notes
TypeSpec 1.10.0 introduces major new extension points for library authors, improves the playground authoring experience, and increases OpenAPI import and export fidelity across several common real-world scenarios.
Overview
- New
extern fnfunctions let libraries compute and transform types or values. - Experimental
internalmodifiers help library authors hide implementation-only declarations. - The playground now supports quick fixes and direct
tspconfig.yamlediting. - OpenAPI tooling gained multi-format output, nested tags, and better import fidelity.
tsp infoand package installation flows are more helpful for day-to-day CLI use.
Highlights
Author libraries with extern fn functions
TypeSpec now supports functions as a new language feature. Library authors can declare host-backed transforms that accept types or values, execute JavaScript logic during checking, and return new types or computed values. This enables extension scenarios that previously required more complex decorator-based patterns.
To use this feature, declare an extern fn signature in TypeSpec, implement it through a $functions export in your JavaScript entrypoint, and call it anywhere a type or value result is allowed.
Example
Library TypeSpec:
import "./lib.js";
namespace Contoso.Functions;
extern fn concat(l: valueof string, r: valueof string): valueof string;Library JavaScript:
export const $functions = { "Contoso.Functions": { concat, },};
function concat(_ctx, l, r) { return `${l}-${r}`;}Consumer TypeSpec:
import "@contoso/functions";using Contoso.Functions;
model Example { name: string = concat("hello", "world");}Related PR: microsoft/typespec#9060 - [compiler] Functions, reborn
Hide package-private declarations with internal
Library and project authors can now mark supported non-namespace declarations as internal. These declarations remain usable within the same package without becoming part of the public API surface. This makes it easier to keep helpers and implementation details private while still exposing public aliases or wrapper types.
Apply the experimental internal modifier to supported declarations that should not be referenced outside the current package.
Example
Library package:
namespace Contoso;
internal model SecretHelper { key: string;}
model PublicApi { data: SecretHelper;}
alias ExposedHelper = SecretHelper;Consumer package:
import "@contoso/library";
model Consumer { helper: Contoso.SecretHelper; // Error: SecretHelper is internal data: Contoso.PublicApi; exposed: Contoso.ExposedHelper;}Related PR: microsoft/typespec#9762 - [compiler]internal symbols
Edit raw tspconfig.yaml directly in the playground
The playground settings experience has been redesigned around a config panel, so advanced scenarios no longer depend on a fixed-form UI. This makes it easier to experiment with project-style configuration, especially when you need emitter options that are too complex or too new for specialized forms.
Open the playground config panel and edit the generated tspconfig.yaml directly to control emitters, options, and other compiler settings.
Example
In the playground config panel, you can edit the same tspconfig.yaml you would keep in a real project.
emit: - "@typespec/openapi3"options: "@typespec/openapi3": file-type: json output-file: openapi.jsonRelated PR: microsoft/typespec#9843 - Playground config as yaml
Apply compiler quick fixes from playground diagnostics
Quick fixes are now available as Monaco code actions in the playground when your cursor is on a diagnostic. This makes the playground a more effective teaching and troubleshooting environment by letting you discover and apply suggested fixes without leaving the browser.
Place the cursor on a diagnostic in the playground editor and use the available code action to apply the suggested fix.
Related PR: microsoft/typespec#9819 - Add codefix support in the TypeSpec playground
Emit OpenAPI JSON and YAML in one run
The OpenAPI 3 emitter can now generate multiple file formats from a single compilation by accepting an array for file-type. This is especially useful when teams want machine-friendly JSON and human-friendly YAML artifacts without maintaining separate build steps.
Set the OpenAPI 3 emitter file-type option to an array of formats, such as both JSON and YAML. If you want distinct filenames, use the output file template.
Example
A single compile can now write both formats. Including {file-type} in output-file keeps the filenames distinct.
emit: - "@typespec/openapi3"options: "@typespec/openapi3": file-type: - json - yaml output-file: "{service-name-if-multiple}.{version}.openapi.{file-type}"Related PR: microsoft/typespec#9890 - Allow @typespec/openapi3 to receive more than one file-type.
Model OpenAPI 3.2 tag hierarchies with parent
You can now express nested tags for OpenAPI 3.2 by setting parent inside @tagMetadata. This makes it possible to preserve tag hierarchies in generated OpenAPI 3.2 documents while safely omitting the field for older OpenAPI targets that do not support it.
Add parent to the metadata for a child tag when targeting OpenAPI 3.2.
Example
import "@typespec/openapi";using TypeSpec.OpenAPI;
@service@tagMetadata("Tag Name", #{ description: "Tag description" })@tagMetadata("Child Tag", #{ description: "Child tag description", parent: "Tag Name" })namespace PetStore;Related PR: microsoft/typespec#9577 - Add support for OAS 3.2 nested tags via parent field in @tagMetadata
Inspect emitter and library options with tsp info
The CLI can now display detailed information for a specific library or emitter, including available options. This reduces guesswork during configuration and makes it faster to discover supported settings from the command line.
Run tsp info <package-name> to inspect a specific package instead of only viewing compiler-wide information.
Example
tsp info @typespec/openapi3Related PR: microsoft/typespec#9829 - Emitter options cli info
Bug fixes
Preserve sibling keywords next to $ref when importing OpenAPI 3.1+
The OpenAPI import tool now preserves supported JSON Schema 2020-12 sibling keywords alongside $ref, including defaults, constraints, descriptions, and deprecation flags. This closes an important fidelity gap where imported TypeSpec definitions could silently lose metadata present in the source OpenAPI document.
Import OpenAPI 3.1 or 3.2 documents as usual; supported sibling keywords next to $ref now flow into the generated TypeSpec.
Example of the fix
parameters: - name: order in: query schema: $ref: "#/components/schemas/OrderEnum" default: "desc"@query order?: OrderEnum = OrderEnum.descKeep nullable array constraints during OpenAPI import
When an OpenAPI schema represents a nullable array with anyOf plus null, outer minItems and maxItems constraints are now preserved. Imported TypeSpec no longer drops these validators on common nullable-array shapes.
Import OpenAPI documents as usual; nullable arrays expressed with anyOf and null now carry over their array size constraints.
Example of the fix
bar: anyOf: - type: array items: type: string - type: "null" minItems: 2 maxItems: 10@minItems(2)@maxItems(10)bar: string[] | null;Use a custom npm registry for tsp init and tsp install
TypeSpec can now use a custom npm registry through the TYPESPEC_NPM_REGISTRY environment variable when fetching manifests and packages. This helps teams working in corporate environments where the public registry is inaccessible or mirrored behind internal infrastructure.
Set TYPESPEC_NPM_REGISTRY before running tsp init or tsp install so the CLI resolves and downloads packages from your approved registry.
Example of the fix
export TYPESPEC_NPM_REGISTRY=https://registry.example.com/npm/tsp initexport TYPESPEC_NPM_REGISTRY=https://registry.example.com/npm/tsp installFix @overload validation inside versioned namespaces
The compiler no longer incorrectly rejects overloads when versioning mutators clone the containing namespace or interface. Projects that combine @versioned with overload-based APIs should now compile without false overload-same-parent errors.
Avoid crashes on unrecognized custom scalar initializers
Compiler and emitter flows that serialize examples or default values no longer fail with an internal compiler error when they encounter custom scalar initializers that have no recognized JSON representation. Values that cannot be serialized are now skipped safely so the rest of the flow can continue.
Generate correct ASP.NET Core result methods for non-200 responses
The C# HTTP server emitter now preserves declared status codes instead of defaulting generated controllers to Ok(...) or NoContent(). Generated controllers use Accepted(...) for 202 responses and StatusCode(...) for other non-200 cases, keeping service behavior aligned with the TypeSpec contract.
Define non-200 or non-204 response status codes in your TypeSpec service and regenerate with @typespec/http-server-csharp.
Example of the fix
For a declared 202 response, the generated controller now preserves that status instead of always returning Ok(...).
model AcceptedResponse { @statusCode statusCode: 202; jobId: string;}
@postop startJob(): AcceptedResponse;return Ok(result);return Accepted(result);Run multiple versioning mutators together reliably
The versioning library now handles multiple mutators in the same flow correctly. This fixes a class of failures where combining versioning transformations could produce incorrect results or unstable processing.
Stop inserting / before ? and : route fragments
Route joining now preserves fragments that start with ? or : instead of rewriting them as path segments. This fixes routes that previously picked up an unwanted leading slash when query-style or colon-prefixed fragments were composed.
You can use route fragments that begin with ? or : without them being rewritten to include an extra slash.
Example of the fix
Query-style and colon-prefixed route fragments now stay attached to the existing route instead of becoming a new path segment.
@route("/pets")namespace PetRoutes { @get @route("?type=cat") op list(): void;}Before:
/pets/?type=catAfter:
/pets?type=catAdditional improvements
Import readOnly and writeOnly as visibility decorators
OpenAPI import now preserves readOnly and writeOnly intent by converting them to @visibility(Lifecycle.Read) and @visibility(Lifecycle.Create). This improves round-tripping and reduces manual cleanup after importing OpenAPI descriptions into TypeSpec.
Run the OpenAPI import flow as usual; imported models now include visibility decorators when those schema flags are present, and conflicting readOnly plus writeOnly inputs are ignored with a warning.
Example
OpenAPI input:
components: schemas: Widget: type: object properties: id: type: string readOnly: true secret: type: string writeOnly: trueImported TypeSpec:
model Widget { @visibility(Lifecycle.Read) id: string;
@visibility(Lifecycle.Create) secret: string;}Programmatically set OpenAPI operation IDs
The OpenAPI library now exposes setOperationId, giving library authors and tooling code a supported API for assigning operation IDs instead of relying on indirect workarounds.
Use the exported setOperationId helper from @typespec/openapi when your library or tooling needs to assign or override an operation ID.
Generate Java Duration support for millisecond encodings
The Java HTTP client emitter now understands DurationKnownEncoding.milliseconds. Duration properties and parameters encoded as integer or floating-point milliseconds continue to surface as Duration in generated clients while being converted correctly on the wire.
Use DurationKnownEncoding.milliseconds in your TypeSpec definitions when targeting the Java HTTP client emitter.
Example
The generated Java surface stays on Duration, while the wire value remains numeric milliseconds.
@route("/int32-milliseconds")@scenario@scenarioDoc(""" Expected query parameter `input=36000` """)op int32Milliseconds( @query @encode(DurationKnownEncoding.milliseconds, int32) input: duration,): NoContentResponse;import java.time.Duration;
private static final Duration MILLIS36000 = Duration.ofMillis(36000);
queryClient.int32Milliseconds(MILLIS36000);Write per-package API version maps into Java metadata
The Java HTTP client emitter can now emit apiVersions in metadata.json, helping downstream tooling and packaging scenarios preserve the correct version mapping for multi-service or mixed-version outputs.
Build C# extensible enums with ExtensibleEnumDeclaration
Emitter framework authors targeting C# now have a dedicated extensible enum component that produces unbound enum-like structs. This makes it easier to model open-ended service values without hand-rolling the generated shape.
Use ExtensibleEnumDeclaration from the emitter framework C# components when you need extensible enum output instead of a closed enum.
Retire the transitional patch-implicit-optional warning
The temporary migration warning for PATCH implicit optionality has been removed, reducing noise in current HTTP workflows.
Start the playground faster by loading libraries in parallel
Playground startup is now more responsive because library loading happens in parallel instead of serially.
Inspect Symbol-keyed decorator state in the HTML program viewer
The HTML program viewer can now display decorator state stored under JavaScript Symbol keys, making advanced library debugging more complete.
Conclusion
If you build TypeSpec libraries, rely on OpenAPI round-tripping, or use the playground for everyday experimentation, 1.10.0 closes several long-standing workflow gaps while adding meaningful new extension capabilities.
Thank you to everyone who contributed feedback and fixes for version 1.10.0.