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 integer(minItems/maxItems)或valueof RangeLimitableTypes(minValue/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。
标量构造函数
标量构造函数通过“在标量引用后加括号”的方式创建标量值。对于从numeric、string、boolean派生的标量,直接调用即可:
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:
| 标量 | 构造函数 | 示例 |
|---|---|---|
plainDate | fromISO/now | plainDate.fromISO("2024-05-06") |
plainTime | fromISO/now | plainTime.fromISO("12:34") |
utcDateTime | fromISO/now | utcDateTime.fromISO("2024-05-06T12:20-12Z") |
offsetDateTime | fromISO/now | offsetDateTime.fromISO("2024-05-06T12:20-0700") |
duration | fromISO | duration.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 可以看到,getValueFromIndeterminate对UnionVariant的处理是递归下钻到其底层类型再判断:若底层是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),仅供参考