Skip to content

1.12.0

TypeSpec 1.12.0 adds more flexible configuration, improves everyday workflows in Playground and VS Code, and strengthens correctness across generated HTTP, OpenAPI, Protobuf, and Python outputs.

Thank you to everyone who contributed feedback and fixes for version 1.12.

Linter authors can now define typed rule options with defaults, and projects can override those options directly in tspconfig.yaml or rulesets. This makes shared linting policies much more practical for teams that want common standards with room for project-specific tuning.

Define defaultOptions when creating a rule, then enable the rule with either true or an options object in your linter configuration.

Example

Rule authors define defaults once, and projects can either enable the rule as-is or override its options.

const myRule = createRule({
name: "no-model-with-name",
severity: "warning",
description: "Bans models with a specific name",
messages: { default: "This model name is not allowed" },
defaultOptions: { bannedName: "Foo" },
create(context) {
return {
model: (target) => {
if (target.name === context.options.bannedName) {
context.reportDiagnostic({ target });
}
},
};
},
});
linter:
enable:
"@typespec/my-lib/no-model-with-name": true
linter:
enable:
"@typespec/my-lib/no-model-with-name":
bannedName: "Bar"

Related PR: microsoft/typespec#10352 - Add rule options

Protobuf output now preserves optional field presence more accurately

Section titled “Protobuf output now preserves optional field presence more accurately”

Optional TypeSpec properties now map to protobuf optional for eligible scalar and enum fields, allowing generated .proto files to preserve explicit presence where protobuf supports it. Message fields retain protobuf’s existing presence behavior, and lossy cases such as repeated or map fields now produce warnings instead of implying semantics protobuf cannot represent.

Use ? on protobuf model properties as usual, regenerate your .proto files, and review any warnings for optional arrays or maps.

Example

Scalars and enums now emit optional; message fields keep their normal presence behavior, and optional arrays still cannot preserve explicit presence.

enum Status {
active: 0,
disabled: 1,
}
model Details {
@field(1)
id: int32;
}
model Example {
// int32 is a scalar type, so this optional property will emit `optional` in protobuf.
@field(1)
count?: int32;
// Status is an enum type, so this optional property will emit `optional` in protobuf.
@field(2)
status?: Status;
// Details is a message type, so this optional property will not emit `optional` in protobuf
// (message fields always have explicit presence discipline in protobuf).
@field(3)
details?: Details;
// Arrays cannot preserve explicit presence in protobuf, so this optional property will not emit `optional` in protobuf
// and will produce a warning.
@field(4)
tags?: string[];
}
message Example {
optional int32 count = 1;
optional Status status = 2;
Details details = 3;
repeated string tags = 4;
}

Related PR: microsoft/typespec#10598 - [protobuf] Represent TypeSpec optional properties as optional fields

The Playground now shows compilation progress, preserves the last successful output during transient errors, and highlights changed files and lines after recompilation. This makes it easier to compare generated output and keep working through intermediate failures without losing context.

As you edit and recompile, the output pane keeps prior results visible and clearly marks what changed.

Related PR: microsoft/typespec#10349 - feat(playground): add compilation spinner, diff highlighting, and out…

Plain unions can represent a single HTTP response more cleanly

Section titled “Plain unions can represent a single HTTP response more cleanly”

Operations that return a union of types without explicit status codes or content types are now treated as a single HTTP response, and generated OpenAPI examples stay aligned with that model. This better reflects author intent when a union represents alternate payload shapes for the same response rather than multiple distinct HTTP responses.

Model alternate payload shapes as a plain union when they share the same response metadata, then regenerate your HTTP and OpenAPI output.

Example

model Cat {
kind: "cat";
name: string;
}
model Dog {
kind: "dog";
name: string;
barkVolume: int32;
}
op readPet(): Cat | Dog;
responses:
"200":
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Dog"

Related PR: microsoft/typespec#10180 - Examples multi response

Packages that rely on subpath exports now resolve correctly in bundler and Playground workflows.

OpenAPI discriminator mappings stay complete during circular emission

Section titled “OpenAPI discriminator mappings stay complete during circular emission”

Generated OpenAPI 3.0 and 3.2 output no longer drops the first discriminator mapping entry during circular emission. This keeps discriminated schemas complete and avoids incorrect or incomplete polymorphic output.

Example of the fix

@discriminator("classification")
model Pokemon {
classification: string;
}
model NormalPokemon extends Pokemon {
classification: "normal";
}
model LegendaryPokemon extends Pokemon {
classification: "legendary";
}
model MythicalPokemon extends Pokemon {
classification: "mythical";
}
union PokemonVariant {
normal: NormalPokemon,
legendary: LegendaryPokemon,
mythical: MythicalPokemon,
}
discriminator:
propertyName: classification
mapping:
normal: "#/components/schemas/NormalPokemon"
legendary: "#/components/schemas/LegendaryPokemon"
mythical: "#/components/schemas/MythicalPokemon"

The Python emitter no longer maps TypeSpec numeric to int. Generated clients now use float, avoiding unsound typing for values that may include fractional data.

Example of the fix

op example(): numeric;
def example(self) -> float: ...

Python paging annotations stay correct when an operation is named list

Section titled “Python paging annotations stay correct when an operation is named list”

Generated paging methods now use the List alias consistently when the operation is named list and page items are themselves collections. This avoids confusing or incorrect type annotations in generated Python code.

Example of the fix

List = list
def list(
self,
*,
cls: ClsType[List[List[str]]],
) -> AsyncItemPaged[List[str]]: ...

tsp format no longer crashes on comment-only parameter lists

Section titled “tsp format no longer crashes on comment-only parameter lists”

Formatting now succeeds when an operation parameter list contains only a dangling block comment, and that comment is preserved.

Example of the fix

namespace MyApp;
op find(/* conditions */): unknown;
namespace MyApp;
op find(/* conditions */): unknown;

Playground diff highlighting is visible and typing is smoother

Section titled “Playground diff highlighting is visible and typing is smoother”

Line-level diff highlighting in the Playground output editor now renders correctly, and redundant recompilations are coalesced while a compile is already running. This reduces missed visual feedback and helps avoid typing stalls during rapid edits.

A number of issues around hover, completion, preview, and symbols have been fixed in the VS Code extension. Quick fixes resolve more reliably, and startup progress no longer blocks the UI as a notification.

Related PRs:

Newly scaffolded projects now use the current TypeSpec package versions at initialization time, such as ^1.12.0, instead of a generic placeholder. This makes generated package.json files easier to review and more predictable to reproduce.

Files that import themselves or repeat the same import now produce warnings. These patterns often indicate mistakes, and surfacing them earlier helps keep specs cleaner and easier to maintain.

Example

import "./main.tsp";
import "./other.tsp";
import "./other.tsp";
warning: A file cannot import itself.
warning: Duplicate import of "./other.tsp"

When union expressions wrap, the formatter now prefers a clearer leading-pipe layout. This improves readability in larger specs and reduces manual formatting churn during review.

Example

alias Pet = Cat | Dog | Snake | Bird | Fish | Lizard;
alias Pet = Cat | Dog | Snake | Bird | Fish | Lizard;

Bundler and Playground-style workflows can now load alloy-based emitters, expanding the set of emitters supported in browser-oriented and bundled scenarios.

The older implicit patch-optional behavior is now deprecated. Move to an explicit patch model with optional properties if you want the previous shape, or switch to MergePatchUpdate or MergePatchCreateOrUpdate when you want explicit merge-patch semantics.

Example

Replace the deprecated form with an explicit patch model, or switch to explicit merge-patch semantics.

model Pet {
name: string;
age: int32;
}
@patch(#{ implicitOptionality: true }) op updatePet(@body patch: Pet): void;
model Pet {
name: string;
age: int32;
}
model PetPatch {
name?: string;
age?: int32;
}
@patch op updatePet(@body patch: PetPatch): void;
model Pet {
name: string;
age: int32;
}
@patch op updatePet(@body patch: MergePatchUpdate<Pet>): void;

If you are upgrading embedded Playground integrations, remove the deprecated diff-highlighting option. For HTTP patch operations, plan a migration away from implicit optionality so future updates remain straightforward.

Show all changes
  • #9884 Deprecate use of @patch(#{implicitOptionality: true}).

    Migrate using one of the following patterns depending on intended semantics:

    1. Preserve previous behavior with an explicit patch model (optional properties)
    model Pet {
    name: string;
    age: int32;
    }
    model PetPatch {
    name?: string;
    age?: int32;
    }
    @patch(#{implicitOptionality: true}) op updatePet(@body patch: Pet): void;
    @patch op updatePet(@body patch: PetPatch): void;
    1. Use merge-patch semantics explicitly with MergePatchUpdate<T>
    model Pet {
    name: string;
    age: int32;
    }
    @patch op updatePet(@body patch: MergePatchUpdate<Pet>): void;

    Use MergePatchCreateOrUpdate<T> when the operation supports create-or-update behavior.

  • #10558 Improve formatting of union expressions

  • #10352 Add support for configurable options on linter rules

    Linter rules can now define typed options with defaults using defaultOptions, and users can pass options when enabling rules in tspconfig.yaml or rulesets.

    Defining a rule with options:

    const myRule = createRule({
    name: "no-model-with-name",
    severity: "warning",
    description: "Bans models with a specific name",
    messages: { default: "This model name is not allowed" },
    defaultOptions: { bannedName: "Foo" },
    create(context) {
    return {
    model: (target) => {
    if (target.name === context.options.bannedName) {
    context.reportDiagnostic({ target });
    }
    },
    };
    },
    });

    Configuring options in tspconfig.yaml:

    linter:
    enable:
    # Enable with default options
    "@typespec/my-lib/no-model-with-name": true
    # Enable with custom options
    "@typespec/my-lib/no-model-with-name":
    bannedName: "Bar"
  • #10431 [init] Package dependencies are now populated with the actual latest version at the time (e.g. ^1.11.0)

  • #10581 Added warnings for duplicate imports and self-imports in the same file

    The compiler now warns when a file imports itself or contains duplicate import statements. These are likely mistakes and while they don’t cause errors, they add unnecessary noise.

    import "./main.tsp"; // Warning: A file cannot import itself.
    import "./other.tsp";
    import "./other.tsp"; // Warning: Duplicate import of "./other.tsp"
  • #10180 [API] Operation returning a union of types without status code or content type will be treated as a single response
  • #10598 Map TypeSpec optionality (?) to protobuf optional where appropriate.
    • optional is applied to fields with protobuf scalar types to set explicit presence.
    • optional is not applied to fields with message types, because they always have explicit presence.
    • Attempting to convert a TypeSpec optional property where the type is an array or Protobuf.Map instance produces a warning, because protobuf cannot differentiate between “empty” and “unset” repeated/map-typed fields.
  • #10618 [LSP] Fix code fixes often not running when selected
  • #10567 Fix server crashes caused by undefined symbol declarations: add null checks for getSymNode() in hover, completion, type-details, and type-signature handlers, and use fallback name for empty DocumentSymbol names
  • #10351 Fix formatter crash when an operation’s parameter list contains only a block comment (e.g. op find(/* conditions */): unknown;). Dangling comments in empty parameter lists are now preserved instead of being dropped.
  • #10556 Fix formatting of decorators on operations and augment decorators. Decorators on operations now break to separate lines when the total line exceeds the print width. Augment decorator arguments are now consistently indented when the line breaks, matching TypeScript/prettier function call formatting.
  • #10580 Hide cursor during spinner animation to prevent flicker on Windows
  • #10180 Fix examples when operation return type have union of response mapping to same status code
  • #10268 Fix missing discriminator mapping entry when the first union variant causes a circular emit, affecting both the OpenAPI 3.0 and 3.2 emitters.
  • #10567 Handle unhandled exceptions in VS Code extension: add custom error handler for server crashes with restart notification, wrap commands with graceful exception handling, and add null guards to prevent extension host errors when LSP client is unavailable
  • #10523 Show “Launching TypeSpec language service…” progress in the status bar instead of as a notification to avoid blocking the UI
  • #10527 Ensure operation telemetry events always carry a valid result value. Previously the start-extension event (and any other operation whose callback returned void) was sent with result="undefined" and classified as an error event. The doOperationWithTelemetry callback is now constrained to return ResultCode | Result<...>, so the result is always derived from the operation’s return value.