TypeSpec Versioning 装饰器全解析:用 @typespec/versioning 声明版本化 API
2026/9/19 12:51:37 网站建设 项目流程

TypeSpec Versioning 装饰器全解析:用 @typespec/versioning 声明版本化 API

【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec

TypeSpec 的@typespec/versioning库通过一组声明式装饰器(@added@removed@renamedFrom@madeOptional@useDependency@versioned等)来描述 API 在多个版本之间的演进历史,让同一份 TypeSpec 源码同时表达"当前状态"与"历史变更"。本文以该库官方参考文档(website/src/content/docs/docs/libraries/versioning/reference/decorators.md)为骨架,逐一讲解全部 9 个装饰器的签名、适用目标、参数语义与实战示例,并结合仓库源码(packages/versioning/src/decorators.ts、packages/versioning/src/versioning.ts)揭示其底层实现原理。读完本文,你将能够为自己的服务声明多版本 API、跟踪属性与类型的演进,并让 OpenAPI 等 emitter 按版本输出正确契约。

装饰器概览

@typespec/versioning导出的所有装饰器都位于TypeSpec.Versioning命名空间下。在使用前需安装依赖并引入:

# 在 spec 项目中安装 npm install @typespec/versioning
import "@typespec/versioning";

9 个装饰器按职责可分为三类:

装饰器作用适用目标
@versioned声明命名空间由哪个枚举定义版本Namespace
@useDependency声明对已版本化依赖库的版本选择EnumMember \| Namespace
@added/@removed声明目标在哪个版本被添加 / 移除模型、属性、操作、枚举、联合、标量、接口等
@renamedFrom声明目标在哪个版本被重命名同上
@madeOptional/@madeRequired声明属性在哪个版本变为可选 / 必选ModelProperty
@typeChangedFrom/@returnTypeChangedFrom声明属性类型 / 操作返回类型在哪个版本变更ModelProperty/Operation

从源码看,每个装饰器的实现都遵循同一模式:先通过checkIsVersion校验传入的EnumMember是否属于某个已@versioned的枚举(packages/versioning/src/decorators.ts),再将结果写入program.stateMap。校验失败时会抛出version-not-found诊断,其消息为:The provided version '...' from '...' is not declared as a version enum. Use '@versioned(...)' on the containing namespace.(见 packages/versioning/src/lib.ts)。这意味着所有版本装饰器的第一个参数必须来自@versioned声明的版本枚举。

声明版本体系:@versioned

@versioned是版本化的起点,它把一个命名空间与一个描述版本序列的枚举绑定:

@TypeSpec.Versioning.versioned(versions: Enum)

目标Namespace

参数

名称类型说明
versionsEnum描述受支持版本的枚举

示例

@versioned(Versions) namespace MyService; enum Versions { v1, v2, v3, }

底层实现中,$versioned会为该命名空间构建一个VersionMap,其中每个枚举成员被包装为Version对象(含namevalueenumMemberindexnamespace字段),并存入program.stateMap(VersioningStateKeys.versions)(packages/versioning/src/decorators.ts)。index按枚举声明顺序从 0 递增,是后续"版本先后"比较的基准;枚举成员解析出的值必须唯一,否则触发version-duplicate诊断(packages/versioning/src/lib.ts)。

在实战中,@versioned通常与@service配合使用。官方教程给出了更完整的形态(website/src/content/docs/docs/libraries/versioning/guide.md):

@service(#{ title: "Contoso Widget Manager" }) @versioned(Contoso.WidgetManager.Versions) namespace Contoso.WidgetManager; enum Versions { v1, v2, }

声明库依赖版本:@useDependency

当你的服务依赖另一个已版本化的 TypeSpec 库(例如 Azure.Core)时,@useDependency用来声明"用哪个版本的库":

@TypeSpec.Versioning.useDependency(...versionRecords: EnumMember[])

目标EnumMember | Namespace

参数

名称类型说明
versionRecordsEnumMember[]目标命名空间或版本所依赖的库版本(可多个)

未版本化服务:声明在命名空间上

@useDependency(MyLib.Versions.v1_1) namespace NonVersionedService;

此时整个服务固定使用MyLibv1_1版本。

版本化服务:声明在版本枚举成员上

@versioned(Versions) namespace MyService1; enum Version { @useDependency(MyLib.Versions.v1_1) // V1 use lib v1_1 v1, @useDependency(MyLib.Versions.v1_1) // V2 use lib v1_1 v2, @useDependency(MyLib.Versions.v2) // V3 use lib v2 v3, }

这样即可建立"服务版本 → 依赖库版本"的映射关系:v1/v2 对应MyLibv1_1,v3 起升级到v2

从实现看,$useDependencyNamespaceEnumMember两类目标分别写入useDependencyNamespaceuseDependencyEnum两个 stateMap(packages/versioning/src/decorators.ts)。需要注意的是:@useDependency只能用在未版本化的命名空间上,对于已@versioned的命名空间必须放在版本枚举成员上,否则会触发incompatible-versioned-namespace-use-dependency错误(packages/versioning/src/lib.ts)。

依赖解析发生在resolveVersions/resolveDependencyVersions中:以根命名空间的每个版本为起点,沿getVersionDependencies得到的依赖图逐层解析出每个依赖命名空间应使用的具体版本,最终产出VersionResolution[](packages/versioning/src/versioning.ts)。emitter 正是基于这份解析结果按版本输出契约。

添加与移除:@added 与 @removed

@added

标识目标在哪个版本被添加:

@TypeSpec.Versioning.added(version: EnumMember)

目标Model | ModelProperty | Operation | Enum | EnumMember | Union | UnionVariant | Scalar | Interface

参数

名称类型说明
versionEnumMember目标被添加的版本

示例

@added(Versions.v2) op addedInV2(): void; @added(Versions.v2) model AlsoAddedInV2 {} model Foo { name: string; @added(Versions.v3) addedInV3: string; }

@removed

标识目标在哪个版本被移除,签名、目标与参数结构同@added对称:

@TypeSpec.Versioning.removed(version: EnumMember)

目标Model | ModelProperty | Operation | Enum | EnumMember | Union | UnionVariant | Scalar | Interface

参数

名称类型说明
versionEnumMember目标被移除的版本

示例

@removed(Versions.v2) op removedInV2(): void; @removed(Versions.v2) model AlsoRemovedInV2 {} model Foo { name: string; @removed(Versions.v3) removedInV3: string; }

底层原理:可用性状态机

两个装饰器在实现上是镜像的:$added把版本追加到addedOn状态数组,$removed追加到removedOn状态数组,并且每次追加后都按index升序排序,保证版本记录有序(packages/versioning/src/decorators.ts)。

真正计算"某个类型在某个版本是否可用"的是getAvailabilityMap(packages/versioning/src/versioning.ts),它把每个版本归入四种状态:

状态含义
Unavailable该版本中目标尚不存在
Added该版本中目标首次出现
Available该版本中目标可用(且不是首次出现)
Removed该版本起目标被移除

计算时会结合父类型(model / interface)的添加与移除信息做"隐式继承"处理:例如一个没有任何版本装饰器的类型会继承父类型的添加版本;若某类型先被移除后被重新添加,则在其添加版本之前还会继承父版本的可用性(resolveWhenFirstAddedresolveRemoved,见 packages/versioning/src/versioning.ts)。官方教程中的例子验证了这一点:v3 中把name重命名为description并改为可选后,v3 的 OpenAPI 输出description,而 v1/v2 的 OpenAPI 仍输出name且为必选(website/src/content/docs/docs/libraries/versioning/guide.md)。

重命名:@renamedFrom

标识目标在哪个版本被重命名,并保留旧名称:

@TypeSpec.Versioning.renamedFrom(version: EnumMember, oldName: valueof string)

目标Model | ModelProperty | Operation | Enum | EnumMember | Union | UnionVariant | Scalar | Interface

参数

名称类型说明
versionEnumMember目标被重命名的版本
oldNamevalueof string目标之前的名称

示例

@renamedFrom(Versions.v2, "oldName") op newName(): void;

实现要点(packages/versioning/src/decorators.ts):

  • oldName不能是空字符串,否则触发invalid-renamed-from-value错误(@renamedFrom.oldName cannot be empty string.);
  • 多个重命名记录按版本升序存入renamedFrom状态数组,并通过getRenamedFrom/getRenamedFromVersions供查询;
  • 若重命名后的名称与同版本已有属性冲突,会触发renamed-duplicate-property错误。

可选性变更:@madeOptional 与 @madeRequired

@madeOptional

标识属性在哪个版本变为可选:

@TypeSpec.Versioning.madeOptional(version: EnumMember)

目标ModelProperty

参数

名称类型说明
versionEnumMember目标变为可选的版本

示例

model Foo { name: string; @madeOptional(Versions.v2) nickname?: string; }

@madeRequired

标识属性在哪个版本变为必选:

@TypeSpec.Versioning.madeRequired(version: EnumMember)

目标ModelProperty

参数

名称类型说明
versionEnumMember目标变为必选的版本

示例

model Foo { name: string; @madeRequired(Versions.v2) nickname: string; }

两个装饰器的实现都是把版本直接写入madeOptional/madeRequired两个 stateMap(packages/versioning/src/decorators.ts)。同时校验器会检查声明与声明结果的一致性(packages/versioning/src/lib.ts):

  • @madeOptional标记的属性在当前代码里必须是可选的(name?写法),否则报made-optional-not-optional
  • @madeRequired标记的属性在当前代码里必须是必选的,否则报made-required-optional

这与版本化库的核心理念一致:TypeSpec 源码永远表达 API 的"当前状态",装饰器只是记录这个状态是从哪个版本开始生效的。

类型变更:@typeChangedFrom 与 @returnTypeChangedFrom

@typeChangedFrom

声明模型属性的类型从某个版本开始变更,同时保持更早版本使用旧类型:

@TypeSpec.Versioning.typeChangedFrom(version: EnumMember, oldType: unknown)

目标ModelProperty

参数

名称类型说明
versionEnumMember类型变更生效的版本;从该版本起使用新类型,更早版本使用旧类型
oldTypeunknown指定版本之前使用的旧类型

示例

model Foo { // In v1: id is a string // In v2+: id is an int32 @typeChangedFrom(Versions.v2, string) id: int32; }

@returnTypeChangedFrom

声明操作的返回类型从某个版本开始变更,更早版本保持旧返回类型:

@TypeSpec.Versioning.returnTypeChangedFrom(version: EnumMember, oldType: unknown)

目标Operation

参数

名称类型说明
versionEnumMember返回类型变更生效的版本;从该版本起使用新返回类型,更早版本使用旧返回类型
oldTypeunknown指定版本之前使用的旧返回类型

示例

// In v1: returns a string // In v2+: returns an int32 @returnTypeChangedFrom(Versions.v2, string) op getUserId(): int32;

实现层面,两者都把(版本 → 旧类型)的映射写入各自 stateMap,并按版本index排序(packages/versioning/src/decorators.ts)。查询函数getTypeChangedFromgetReturnTypeChangedFrom返回该映射,getAvailabilityMap在计算可用性时也会读取它们,作为"版本信息存在"的依据(packages/versioning/src/versioning.ts)。

组合使用:一个完整的版本化示例

将上述装饰器组合起来,即可表达真实世界的 API 演进。以下改编自官方教程(website/src/content/docs/docs/libraries/versioning/guide.md):

using TypeSpec.Versioning; using TypeSpec.Http; using TypeSpec.Rest; @service(#{ title: "Contoso Widget Manager" }) @versioned(Contoso.WidgetManager.Versions) namespace Contoso.WidgetManager; enum Versions { v1, v2, // v2 新增 get 操作 v3, // v3 重命名并可选化 description } model Widget { @key id: string; // v3 起由 name 重命名为 description,并变为可选 @renamedFrom(Versions.v3, "name") @madeOptional(Versions.v3) description?: string; } @route("/widget") op list(): Widget[] | Error; // v2 才引入的操作 @added(Versions.v2) @route("/widget/{id}") op get(...Resource.KeysOf<Widget>): Widget | Error;

这段代码生成 v3 的 OpenAPI 时Widget包含id(必选)与description(可选);而 v1/v2 的 OpenAPI 中仍然是name(必选)。装饰器越多,历史版本的信息越完整,这正是 emitter 能够按版本输出不同契约的基础。

相关文档与源码索引

  • 官方参考文档:website/src/content/docs/docs/libraries/versioning/reference/decorators.md
  • 入门教程(含 OpenAPI 输出对比):website/src/content/docs/docs/libraries/versioning/guide.md
  • 库概览与安装:website/src/content/docs/docs/libraries/versioning/reference/index.mdx
  • 装饰器 TypeSpec 声明:packages/versioning/lib/decorators.tsp
  • 装饰器实现(stateMap 读写与校验):packages/versioning/src/decorators.ts
  • 版本可用性计算(Availability 状态机):packages/versioning/src/versioning.ts
  • 诊断信息定义(错误码与消息模板):packages/versioning/src/lib.ts
  • 版本时间线建模:packages/versioning/src/versioning-timeline.ts
  • 版本解析结果类型定义:packages/versioning/src/types.ts

【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询