news 2026/9/19 1:48:17

TypeSpec 值(Value)体系完全指南:对象值、数组值、标量值与 `valueof` 约束

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeSpec 值(Value)体系完全指南:对象值、数组值、标量值与 `valueof` 约束

TypeSpec 值(Value)体系完全指南:对象值、数组值、标量值与valueof约束

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

TypeSpec 语言在类型系统之外提供了一套独立的“值(Value)”体系,用于表达默认值、示例数据、装饰器参数与模板实参。本篇以 语言基础文档 values.md 为骨架,系统讲解四种值类型、字面量与上下文的语义切换、const声明、typeof运算符、值校验以及枚举成员/联合变体引用规则,并深入 编译器源码 印证其底层实现。读完本文,你将能准确判断一段字面量“何时是类型、何时是值”,并熟练用#{}#[]、标量构造函数与valueof写出正确的 TypeSpec 代码。

值(Value)与类型(Type):两个相互独立的世界

TypeSpec 除了可以定义类型,还可以定义值。在 API 描述中,值主要用在四个场景:

  • 为类型定义默认值(如属性默认值);
  • 提供示例值(如@example);
  • 装饰器传参(如@maxItems(2)中的2);
  • 作为最终会传给装饰器或被用作默认值的模板参数

核心规则是:值不能当作类型使用,类型也不能当作值使用,二者是完全分离的两个实体。例如下面的代码是错误的:

const example = #{ prop1: #{ nested: true }, // ok:对象值属性必须指向另一个值 prop2: { nested: true, }, // error:模型表达式是类型,不能放在对象值里 prop3: string, // error:string 是类型,不能作为对象值属性 };

不过有两类“特殊存在”可以根据上下文在类型与值之间切换:

  • 标量字面量(string / number / boolean / null 字面量):视所处上下文不同,可能是一个类型或一个值(见下文 标量字面量);
  • 枚举成员引用与联合变体引用:同样视上下文在类型与值之间切换(见下文 枚举成员与联合变体引用)。

在编译器的类型模型中,这一分离体现在 types.ts 中定义的Value联合类型:它由ScalarValue | NumericValue | StringValue | BooleanValue | ObjectValue | ArrayValue | EnumValue | NullValue | FunctionValue组成,与Type联合类型并列为两棵独立的实体树。

值的四种基本形态(Value kinds)

值共有四种基本形态:对象值(object)数组值(array)标量值(scalar)null,分别通过对象值语法、数组值语法、标量字面量/标量构造函数和null字面量创建。此外,引用枚举成员与联合变体也会产生值。

对象值(Object values):#{}

对象值使用#{}语法,可定义任意数量的属性:

const point = #{ x: 0, y: 0 };

对象值的每个属性必须引用其他值,引用类型(如模型、string)都是编译错误。其底层结构见 types.ts:ObjectValue内部通过Map<string, ObjectValuePropertyDescriptor>保存属性名到属性值(value: Value)的映射,ObjectValuePropertyDescriptor还记录了可选的 AST 节点便于诊断定位。

数组值(Array values):#[]

数组值使用#[]语法,可包含任意数量的元素:

const points = #[#{ x: 0, y: 0 }, #{ x: 1, y: 1 }];

与对象值一样,数组值内部不能包含类型。ArrayValue在 types.ts 中定义为values: Value[],元素同样只能是值。

如果数组类型通过@minValue/@maxValue(以及更常见的@minItems/@maxItems)声明了最小/最大元素个数,编译器会在给该类型赋数组值时做数量校验

/** Can have at most 2 tags */ @maxItems(2) model Tags is Array<string>; const exampleTags1: Tags = #["TypeSpec", "JSON"]; // ok const exampleTags2: Tags = #["TypeSpec", "JSON", "OpenAPI"]; // error:超出最大元素数

这些校验装饰器定义于 std/decorators.tsp,其参数声明为valueof integerminItems/maxItems)或valueof RangeLimitableTypesminValue/maxValue),即要求调用方传入而非类型。

标量值(Scalar values)

创建标量值有两种方式:字面量语法(如"string value")与标量构造函数(如utcDateTime.fromISO("2020-12-01T12:00:00Z"))。

标量字面量

字符串、数值、布尔与null的字面量会根据所在上下文被解释为类型或值:

  • 类型上下文(type context):模型属性类型、操作返回类型、别名定义等位置,字面量成为字面量类型(literal type),例如model A { x: 123 }中的123是数值字面量类型;
  • 值上下文(value context):默认值、对象值的属性、const定义等位置,字面量成为
  • 模糊上下文(ambiguous context):模板或装饰器参数(可同时接受类型或值)中,字面量默认解释为;如需在此时把它显式作为类型传递,可使用typeof运算符转换。

下面的示例展示了三种上下文对装饰器实参的影响:

// 示例装饰器签名,仅为演示,无实际实现。 extern dec setNumberValue(target: unknown, color: valueof numeric); extern dec setNumberType(target: unknown, color: numeric); extern dec setNumberTypeOrValue(target: unknown, color: numeric | (valueof numeric)); @setNumberValue(123) // 传入标量值 numeric(123) @setNumberType(123) // 传入数值字面量类型 123 @setNumberTypeOrValue(123) // 模糊上下文 → 传入标量值 numeric(123) model A {}

从实现上看,字面量节点在检查(check)阶段先被求值为一种“不定态(Indeterminate)”,再由约束决定落到类型还是值分支。核心逻辑在 checker.ts 的getValueForNode与 getValueFromIndeterminate:对于String/Number/Boolean/EnumMember/UnionVariant/null等既可以当类型又可以当值的实体,会依据CheckValueConstraint决定是否转为值。若在期望值的位置传入了纯类型,编译器会抛出expect-value诊断,其消息会给出修正建议(见 messages.ts):

"${name}" refers to a model type, but is being used as a value here. Use #{} to create an object value.Is a tuple type, but is being used as a value here. Use #[] to create an array value.

这也解释了为何模型表达式{ nested: true }报错而对象值#{ nested: true }合法——编译器甚至提供了把{}自动修正为#{}、把元组自动修正为#[]的 code fix。

标量构造函数

标量构造函数通过“在标量引用后加括号”的方式创建标量值。对于从numericstringboolean派生的标量,直接调用即可:

const n = int8(100); const s = string("hello");

任何标量还可以声明具名构造函数(named constructor),接收一个或多个值参数。例如utcDateTime提供了接收 ISO 字符串的fromISO构造函数。自定义具名构造函数用init关键字声明:

scalar ipv4 extends string { init fromInt(value: uint32); } const ip = ipv4.fromInt(2341230);

内置的时间类标量都预置了具名构造函数,定义见 intrinsics.tsp:

标量构造函数示例
plainDatefromISO/nowplainDate.fromISO("2024-05-06")
plainTimefromISO/nowplainTime.fromISO("12:34")
utcDateTimefromISO/nowutcDateTime.fromISO("2024-05-06T12:20-12Z")
offsetDateTimefromISO/nowoffsetDateTime.fromISO("2024-05-06T12:20-0700")
durationfromISOduration.fromISO("P1Y1D")

在检查器层面,createScalarValue(见 checker.ts)负责核对实参个数(必选参数、可选参数、rest 参数)、逐一以valueof约束求值实参,并对个数不匹配的情况报告invalid-argument-count诊断。而像 examples.ts 这样的下游模块则通过value.value.name === "fromISO"判断并序列化这些构造函数产生的值。

Null 值

null值通过null字面量创建:

const value: string | null = null;

null值与null类型一样,在 TypeSpec 语言中没有任何特殊行为,它仅仅是像 JSON 中那样的null值。在类型模型中对应 NullValue,其value字段恒为null

Const 声明

const声明把值保存到变量中供后续引用。const可以带可选类型注解;当类型注解缺省时,编译器会从初始化式构造一个精确类型(exact type)作为推断类型:

const stringValue: string = "hello"; // ^-- type: string const oneValue = 1; // ^-- type: 1 const objectValue = #{ x: 0, y: 0 }; // ^-- type: { x: 0, y: 0 }

可见无注解的const oneValue = 1推断出的类型是字面量类型1而非numeric#{ x: 0, y: 0 }推断为{ x: 0, y: 0 }。带注解的const则按注解类型存储(如string)。const节点的检查逻辑见 checker.ts:先对初始化式求值,若提供了类型注解则校验值可赋给该类型,并通过copyValue(value, { type })把注解类型记录为值的存储类型(storage type)。

typeof运算符

typeof运算符返回某个值引用的声明类型或推断类型。注意:变量实际存储的值可能比声明类型更具体——例如用联合类型声明的const,其值在任意时刻只会是联合中的某一个变体,但typeof返回的是声明的联合类型

const stringValue: string = "hello"; // typeof stringValue 返回 `string` const oneValue = 1; // typeof oneValue 返回 `1` const stringOrOneValue: string | 1 = 1; // typeof stringOrOneValue 返回 `string | 1`

在模糊上下文中,typeof也是把“被当作值的字面量”显式恢复为类型的常用手段,与 标量字面量 一节中的setNumberType(123)场景互补。

值校验(Validation)

TypeSpec 会用@minLength@maxValue等内置校验装饰器对值进行验证。校验既作用于直接赋值的const,也递归作用于对象值/数组值内部的元素:

@maxLength(3) scalar shortString extends string; const s1: shortString = "abc"; // ok const s2: shortString = "abcd"; // error:超过最大长度 model Entity { a: shortString; } const e1: Entity = #{ a: "abcd" }; // error:对象值内层属性同样参与校验

这类装饰器在 std/decorators.tsp 中均声明为valueof参数(如extern dec maxLength(target: string | ModelProperty, value: valueof integer)),即它们接收的是值。编译器在赋值检查路径(checkValueOfType,见 checker.ts)之外,还会对值执行约束校验,从而保证默认值、示例值与装饰器参数在编译期即符合类型声明的约束。

枚举成员与联合变体引用

枚举成员引用遵循与标量字面量相同的上下文规则:在类型上下文中,引用成为枚举成员类型Reflection.EnumMember);在值上下文或模糊上下文中,引用成为该成员对应的值

extern dec setColorValue(target: unknown, color: valueof string); extern dec setColorMember(target: unknown, color: Reflection.EnumMember); enum Color { red, green, blue, } @setColorValue(Color.red) // 等价于传入字面量 "red" @setColorMember(Color.red) // 传入枚举成员 Color.red model A {}

联合变体引用的规则类似:类型上下文得到该变体的类型;值上下文或模糊上下文得到该变体的值。但有一条硬性限制:引用其类型不是字面量类型的联合变体作为值使用是错误(因为无法把任意类型降级成一个具体的值)。

extern dec setColorValue(target: unknown, color: valueof string); extern dec setColorType(target: unknown, color: string); union Color { red: "red", green: "green", blue: "blue", other: string, // 类型不是字面量 } @setColorValue(Color.red) // 传入标量值 string("red") @setColorValue(Color.other) // error:试图把类型当值传递 @setColorType(Color.red) // 传入字符串字面量类型 "red" model A {}

从 checker.ts 可以看到,getValueFromIndeterminateUnionVariant的处理是递归下钻到其底层类型再判断:若底层是String/Number/Boolean等可值化的字面量类型则成功转为值,否则维持类型原样并在后续的期望值检查中报错。

结合测试验证:valueof如何把值传给装饰器

编译器测试 decorators.test.ts 直接验证了值到 JS 实参的转换行为:

// valueof {name: string} + #{name: "foo"} → JS 对象 { name: "foo" } // valueof {name: unknown} + #{name: #{other: "foo"}} → 递归转换为 { name: { other: "foo" } } // valueof string[] + #["foo"] → JS 数组 ["foo"] // valueof unknown[] + #[#["foo"]] → 递归转换为 [["foo"]]

测试还覆盖了__proto__constructor等特殊属性名的安全性(对象值在转换为 JS 对象时这些成员仍保留为自有属性,避免原型污染),说明对象值到运行时数据的序列化是递归且安全的。这从侧面印证了值体系不仅是语法糖,还承担着“把声明式数据安全地交给装饰器实现”的职责。

小结

TypeSpec 的值体系可以概括为三点:语法上,用#{}写对象值、用#[]写数组值、用字面量与标量构造函数写标量值、用null写空值;语义上,标量字面量、枚举成员与联合变体引用会根据类型/值/模糊上下文自动切换身份,typeof用于显式取回类型;用途上,值服务于默认值、示例、装饰器实参与模板实参,并由@minLength@maxItems@minValue等内置装饰器在编译期完成校验。理解并善用这套体系,是写出规范、可验证的 TypeSpec API 描述的关键一步。

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 1:48:12

RouterOS家庭网络内容过滤实战:从DNS黑洞到L7三层拦截

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 1:46:03

电子政务运维经费测算:从资产台账到财政评审的完整路径

简介&#xff1a;深圳市电子政务项目运行维护经费指导意见&#xff08;2007年发布&#xff09;是一份供有关部门编制电子政务运维预算时参照的官方标准文件&#xff0c;旨在规范运维经费测算与申报流程。包内共1个PDF文件&#xff0c;大小约70KB&#xff0c;完整收录了《深圳市…

作者头像 李华
网站建设 2026/9/19 1:40:52

MCP、Skill、Plugin 别再混用:Agent 扩展的三层选型与协作

上周有个做后端的哥们儿在群里问&#xff0c;他想让手里的 agent 能查公司内部接口文档&#xff0c;顺带按团队规范生成代码。有人让他写 MCP&#xff0c;有人说写个 Skill 就够了&#xff0c;还有人直接甩了篇 Plugin 开发教程过来。三份文档他都看了&#xff0c;结果比看之前…

作者头像 李华
网站建设 2026/9/19 1:39:47

Windows下Maven环境变量配置与mvn命令不被识别排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 1:37:55

轮式机器人直线控制:陀螺仪+PID航向闭环实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华