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 fn functions let libraries compute and transform types or values.
  • Experimental internal modifiers help library authors hide implementation-only declarations.
  • The playground now supports quick fixes and direct tspconfig.yaml editing.
  • OpenAPI tooling gained multi-format output, nested tags, and better import fidelity.
  • tsp info and 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.json

Related 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

Terminal window
tsp info @typespec/openapi3

Related 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.desc

Keep 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

Terminal window
export TYPESPEC_NPM_REGISTRY=https://registry.example.com/npm/
tsp init
Terminal window
export TYPESPEC_NPM_REGISTRY=https://registry.example.com/npm/
tsp install

Fix @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;
}
@post
op 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=cat

After:

/pets?type=cat

Additional 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: true

Imported 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.

TypeSpec Team

TypeSpec Team @ Microsoft