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/versioningimport "@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
参数:
| 名称 | 类型 | 说明 |
|---|---|---|
| versions | Enum | 描述受支持版本的枚举 |
示例:
@versioned(Versions) namespace MyService; enum Versions { v1, v2, v3, }底层实现中,$versioned会为该命名空间构建一个VersionMap,其中每个枚举成员被包装为Version对象(含name、value、enumMember、index、namespace字段),并存入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
参数:
| 名称 | 类型 | 说明 |
|---|---|---|
| versionRecords | EnumMember[] | 目标命名空间或版本所依赖的库版本(可多个) |
未版本化服务:声明在命名空间上
@useDependency(MyLib.Versions.v1_1) namespace NonVersionedService;此时整个服务固定使用MyLib的v1_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 对应MyLib的v1_1,v3 起升级到v2。
从实现看,$useDependency对Namespace与EnumMember两类目标分别写入useDependencyNamespace与useDependencyEnum两个 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
参数:
| 名称 | 类型 | 说明 |
|---|---|---|
| version | EnumMember | 目标被添加的版本 |
示例:
@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
参数:
| 名称 | 类型 | 说明 |
|---|---|---|
| version | EnumMember | 目标被移除的版本 |
示例:
@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)的添加与移除信息做"隐式继承"处理:例如一个没有任何版本装饰器的类型会继承父类型的添加版本;若某类型先被移除后被重新添加,则在其添加版本之前还会继承父版本的可用性(resolveWhenFirstAdded、resolveRemoved,见 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
参数:
| 名称 | 类型 | 说明 |
|---|---|---|
| version | EnumMember | 目标被重命名的版本 |
| oldName | valueof 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
参数:
| 名称 | 类型 | 说明 |
|---|---|---|
| version | EnumMember | 目标变为可选的版本 |
示例:
model Foo { name: string; @madeOptional(Versions.v2) nickname?: string; }@madeRequired
标识属性在哪个版本变为必选:
@TypeSpec.Versioning.madeRequired(version: EnumMember)目标:ModelProperty
参数:
| 名称 | 类型 | 说明 |
|---|---|---|
| version | EnumMember | 目标变为必选的版本 |
示例:
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
参数:
| 名称 | 类型 | 说明 |
|---|---|---|
| version | EnumMember | 类型变更生效的版本;从该版本起使用新类型,更早版本使用旧类型 |
| oldType | unknown | 指定版本之前使用的旧类型 |
示例:
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
参数:
| 名称 | 类型 | 说明 |
|---|---|---|
| version | EnumMember | 返回类型变更生效的版本;从该版本起使用新返回类型,更早版本使用旧返回类型 |
| oldType | unknown | 指定版本之前使用的旧返回类型 |
示例:
// In v1: returns a string // In v2+: returns an int32 @returnTypeChangedFrom(Versions.v2, string) op getUserId(): int32;实现层面,两者都把(版本 → 旧类型)的映射写入各自 stateMap,并按版本index排序(packages/versioning/src/decorators.ts)。查询函数getTypeChangedFrom与getReturnTypeChangedFrom返回该映射,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),仅供参考