TypeSpec 值Value体系完全指南对象值、数组值、标量值与valueof约束【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespecTypeSpec 语言在类型系统之外提供了一套独立的“值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, // errorstring 是类型不能作为对象值属性 };不过有两类“特殊存在”可以根据上下文在类型与值之间切换标量字面量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.tsObjectValue内部通过Mapstring, 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 Arraystring; 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。标量构造函数标量构造函数通过“在标量引用后加括号”的方式创建标量值。对于从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标量构造函数示例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)场景互补。值校验ValidationTypeSpec 会用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),仅供参考