TypeSpec 模板(Templates)完全指南:泛型复用、参数约束与 valueof 值参数
TypeSpec 模板Templates完全指南泛型复用、参数约束与 valueof 值参数【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec本篇指南围绕 TypeSpec 语言基础中的模板Templates机制展开讲解如何像泛型一样复用类型定义、通过extends约束模板参数、为参数提供默认值、按名称传参以及借助valueof让模板接收值而非类型。读完本文你将掌握在模型、别名、操作与接口上声明和使用模板的完整语法并能用编译器源码中的校验逻辑与测试用例验证自己的写法是否正确。什么是模板Templates模板是 TypeSpec 中实现类型复用的核心工具允许你“参数化”一个类型的某些方面。它与主流编程语言中的泛型Generics类似模板声明时定义模板参数使用方在引用该类型时再传入具体参数编译器据此生成对应的具体类型实例。模板可以应用于四类声明别名aliases模型models操作operations接口interfaces一个最典型的例子是“分页模型”PageItem接收元素类型Item随后任何具体的数据类型都可以通过实例化PageDog复用同一套结构model PageItem { size: int32; item: Item[]; } model DogPage { ...PageDog; }DogPage通过模型展开语法...继承了PageDog最终等价于{ size: int32; item: Dog[]; }。这种“一次声明、处处复用”的能力正是模板在 API 定义库如 REST、OpenAPI 相关库中被广泛使用的原因。模板参数的默认值模板参数可以声明默认值语法为在参数后追加 value。当调用方省略该参数时编译器自动使用默认值model PageItem string { size: int32; item: Item[]; }此时Page在未传参的情况下等价于Pagestring。默认值机制让模板在“常用场景免参数、特殊场景传参数”之间平滑切换是设计可扩展 API 类型时的重要手法。使用 extends 约束模板参数模板参数可以用extends关键字限定取值范围约束。约束不满足时编译器会在实例化点直接报错。关于约束校验的具体规则可参阅 类型关系type relations 文档。最简单的约束是限制参数必须为某个内建类型alias FooType extends string Type;如果尝试用不满足string约束的参数实例化Foo会得到如下错误alias Bar Foo123; ^ Type 123 is not assignable to type TypeSpec.string模板参数约束还可以是模型表达式用于要求传入的参数满足某种结构。例如要求Type是一个带name: string属性的模型// Expect Type to be a model with property name: string alias FooType extends {name: string} Type;默认值同样必须满足约束否则同样会触发编译错误alias FooType extends string Abc Type; // Invalid alias BarType extends string 123 Type; ^ Type 123 is not assignable to type TypeSpec.string可选参数必须位于末尾所有带默认值可选的模板参数必须排在模板参数列表的末尾必选参数之后不允许再出现可选参数// Invalid alias FooT extends string Abc, U ...; ^ Required template arguments must not follow optional template arguments这一规则在编译器内部被定义为独立错误码。在 packages/compiler/src/core/messages.ts 中可以看到其对应消息为default-required默认文案是 “Required template parameters must not follow optional template parameters”。这保证了实例化时位置参数与默认值填充的语义清晰、无歧义。命名模板参数Named template arguments模板参数除了按位置传递还可以按名称指定。命名传参允许你打乱顺序并且可以跳过某个可选的中间参数——这在模板参数多、默认值多时尤为实用alias TestT, U extends numeric int32, V extends string example { t: T; v: V; }; // Specify the argument V by name to skip argument U, since U is optional and we // are okay with its default alias Example1 Testunknown, V example1; // Even all three arguments can be specified out of order alias Example2 TestV example2, T unknown, U uint64;注意上例中Example2将三个参数全部以名称乱序给出编译器依然能正确解析。命名传参的两个硬性规则规则一一旦某个参数改为按名称指定后续所有参数都必须按名称指定位置参数不能再出现在命名参数之后// Invalid alias Example3 Test V example3, unknown, ^^^^^^^ Positional template arguments cannot follow named arguments in the same argument list. ;该规则在编译器中的错误码为invalid-template-args见 messages.ts其中的positionalAfterNamed分支正是这条报错文案。参数名不存在时也会命中同一错误码的unknownName分支“No parameter named ... exists in the target template.”重复指定同一参数则会触发specifiedAgain“Cannot specify template argument ... again.”。packages/compiler/test/checker/templates.test.ts中就有针对这一语义的完整测试例如测试 “cannot specify positional argument after named argument”templates.test.ts验证了在Aboolean, V bar, string这种“命名参数后跟位置参数”的写法下编译器同时报出invalid-template-args错误与之配套的测试还验证了省略可选参数如Aboolean, V bar时模型属性b: U会正确回落到默认值int32而c: V取到传入的字符串值bar。规则二模板参数名属于模板的公开 API。既然支持按名称传参重命名模板参数就可能导致依赖该模板的既有规格specification无法编译。重命名模板参数可能破坏使用该模板的现有代码属于破坏性变更。参数的求值顺序模板参数按模板定义中参数的声明顺序求值而不是按实例化写法中的书写顺序求值。多数场景下这个差别无足轻重但当事先对模板参数求值可能触发带有副作用side effect的装饰器时顺序就变得重要了——例如装饰器可能依赖先求值的参数来注册元数据此时求值顺序会影响最终结果。模板与值参数Templates with values模板不仅可以接收类型还可以通过valueof约束接收值。这在为装饰器提供参数、或为类型提供默认值时非常有用——因为装饰器需要的往往是具体的字面量值而不是类型。alias TakesValueStringType extends string, StringValue extends valueof string { doc(StringValue) property: StringType; }; alias M1 TakesValuea, b;这里StringType接收类型a字符串字面量类型而StringValue接收值b随后被doc装饰器用作文档字符串。类型或值二选一的“混合约束”当模板参数同时接受类型或值如约束写成string | (valueof string)时如果直接传入字面量或枚举/联合成员引用编译器会将其作为值传递。例如下面的StringTypeOrValue就是一个字符串字面量类型为a的值alias TakesTypeOrValueStringTypeOrValue extends string | (valueof string) { customDecorator(StringOrValue) property: string; }; alias M1 TakesValuea;在编译器实现中这种“类型 值”混合约束被建模为MixedParameterConstraint见 packages/compiler/src/core/checker.ts编译器遍历联合约束中的每个分支分别收集其中的值约束valueof分支与类型约束普通类型分支构造出独立的valueType与type。当两者都存在时传入的实参将按“字面量/成员引用优先作为值”的规则匹配。如果确实需要取回某个值的声明类型可以使用typeof运算符。模板参数的值类型推断当模板以值实例化时值的类型以及typeof运算的结果取决于实参本身而不是模板参数的约束。这与 const 声明的类型推断规则 一致直接传入字符串字面量b那么模板内部StringValue的类型就是字符串字面量类型b传入一个const则值的类型就是该 const 的声明类型。看下面的例子property在M1中的最终类型是a | balias TakesValueStringValue extends valueof string { doc(StringValue) property: typeof StringValue; }; const str: a | b a; alias M1 TakesValuestr;str的声明类型是联合类型a | b因此以str实例化模板后typeof StringValue得到的是a | b而非约束中的string。这正是“值类型由实参决定、而非由约束决定”的直观体现在设计接收值的模板例如把枚举值传给装饰器时尤其要注意。小结与实践建议TypeSpec 的模板机制可以总结为以下几个要点复用范围广模型、别名、操作、接口均支持模板化是构建可复用 API 类型库的基础设施。默认值 约束 value提供默认值extends限定取值范围可以是类型、模型表达式乃至valueof值约束默认值也必须满足约束。参数顺序纪律可选参数必须置于参数列表末尾一旦开始按名称传参后续参数必须全部按名称传参。公开 API 敏感性模板参数名属于公开 API重命名属于破坏性变更。类型与值分离需要给装饰器传字面量值时使用valueof约束字面量与枚举/联合成员引用在“类型或值”混合约束下按值传递值的类型按实参而非约束推断必要时用typeof取回声明类型。这些规则并非纸面约定而是由编译器强制保证的相关错误消息定义在 packages/compiler/src/core/messages.ts实例化逻辑位于 packages/compiler/src/core/checker.ts 的instantiateTemplate行为正确性则由 packages/compiler/test/checker/templates.test.ts 中的大量用例守护。阅读这些源码与测试可以帮助你在编写自己的模板时预判编译器的行为。【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考