ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入理解 TypeScript:泛型的实例化类型——为泛型类与泛型函数生成具体化版本

深入理解 TypeScript:泛型的实例化类型——为泛型类与泛型函数生成具体化版本 文档教程【免费下载链接】typescript-book-chineseTypeScript Deep Dive 中文版项目地址https://gitcode.com/gh_mirrors/ty/typescript-book-chinese点击查看免费下载本篇技术指南聚焦于 深入理解 TypeScript 中文版中「泛型的实例化类型」这一主题当泛型类FooT需要以某个具体类型如Foonumber的形式被复用、注入或导出时如何在保持类型安全的前提下为其生成具体化的版本。读完本文你将掌握三种实例化手段——构造签名类型断言、继承派生、函数签名断言并理解它们各自的适用场景、安全性边界以及与typeof、ConstructorParameters等类型工具的关系。从问题说起泛型类的具体化需求泛型是 TypeScript 类型系统最核心的约束工具其设计目的是在类的实例成员、方法、函数参数与返回值之间提供有意义的类型约束参见 泛型基础。假设你定义了这样一个带泛型参数的类class FooT { foo: T; }此时Foo自身还只是一个类型构造器——只有当你写出Foonumber、Foostring这样的具体类型时它才具备完整的实例类型。但在某些场景下你需要把Foo当作一个具体类型来使用想把Foonumber作为一个独立的类引用传给某个接收类的 API想把某个具体化的类实例化工厂导出给外部模块想在new调用位置直接获得类型明确的实例。这时你就需要对泛型类做一次实例化Instantiation操作。本仓库 docs/tips/typeInstantiation.md 给出了两条主流路径构造签名类型断言与继承派生。方案一构造签名类型断言第一种方式是将泛型类拷贝到一个新变量中并用具体类型替换泛型参数的注解配合类型断言来重塑它的类型class FooT { foo: T; } const FooNumber Foo as { new (): Foonumber }; // ref 1这里ref 1这一行做了三件事as是 TypeScript 的类型断言语法。它与运行时无关纯粹是一个编译时指令告诉编译器你比我更了解这个值的类型请按我给出的方式分析它详见 类型断言。{ new (): Foonumber }是一个可实例化construct signature接口new ()表示该值可以通过new运算符调用调用后返回一个Foonumber类型的实例可调用与可实例化签名的完整语法见 可调用的。整行表达式的语义是FooNumber与Foo是同一个类但编译器将只把它看作使用new调用时产出一个Foonumber实例的构造器。随后你就可以像使用普通类一样使用它并获得完整的类型推导const a new FooNumber(); // a 的类型被推断为 Foonumber a.foo 123; // ok a.foo string; // Error: string 不能赋值给 number断言语法选型as优于尖括号在编写上述代码时请使用as语法而非Foo尖括号语法。尖括号断言在 JSX 环境中会与 JSX 元素产生语法歧义而as在任何环境下都无歧义类型断言 明确建议为了一致性统一使用as。尤其当你需要在.tsx文件中实例化泛型类时as是唯一稳妥的选择。深入原理断言只改变编译视角不改变运行时行为要正确使用方案一必须理解类型断言的两条关键性质第一断言是编译时的不是运行时的。它不像 Java/C# 中的类型转换那样在运行时做任何强转或检查。Foo as { new (): Foonumber }编译后不产生任何额外的运行时指令FooNumber在运行时就是Foo本身。第二断言有一个可赋值性前提。只有当源类型S是目标类型T的子集、或T是S的子集时S才能被成功断言为T类型断言 中关于单个断言是否足够的判定规则。在泛型实例化场景中Foo的构造签名new T(...): FooT与{ new (): Foonumber }之间恰好满足这一兼容关系因此断言是合法的而完全毫无根据的断言例如把Event断言成HTMLElement则会被编译器拒绝除非先经过any做双重断言。这也解释了为什么这种泛型具体化的断言模式可以成立——它建立在结构化类型兼容的基础之上参见 类型兼容性 中泛型在被成员使用时影响兼容性的规则Foonumber与Foostring因成员foo: T的存在而互不兼容这也正是实例化有意义的原因。方案二继承派生类型断言模式存在一个先天弱点编译器无条件信任你。一旦你在断言时写错了具体类型例如把Foostring写成Foonumber编译器不会发出任何警告错误只能留到运行时或后续的类型检查中暴露。在其他语言中为泛型类创建具体化版本的常见模式是继承class FooNumber extends Foonumber {}这种方式的优势在于类型关系由编译器推导和校验FooNumber自动获得Foonumber的全部成员类型对使用方而言FooNumber是一个完全正常的、可独立导出的类保留了后续扩展的余地——你可以在FooNumber上继续添加具体化后的专属成员或方法。const fn new FooNumber(); fn.foo 42; // ok fn.foo x; // Error使用继承时的装饰器警告采用继承方案时需要特别注意如果你在基类上使用了装饰器Decorator继承类可能不具备与基类相同的行为——它不再被装饰器包裹。装饰器是类声明阶段的元编程机制本项目 docs/faqs/decorators.md 对该主题有专门讨论它作用于被装饰的类本身而class FooNumber extends Foonumber {}是另一个全新的类声明基类上的装饰器不会自动传递到子类。因此当基类依赖装饰器注入行为如依赖注入框架、属性元数据时请谨慎选择继承方案必要时改用断言方案或显式地在子类上重复应用装饰器。函数版本的实例化调用签名断言如果你并不需要生成一个单独的类泛型的实例化同样适用于函数。原文档给出了一个简洁的通用模式——先把泛型函数断言成具体的调用签名再赋给新变量function idT(x: T) { return x; } const idNum id as { (x: number): number };这里{ (x: number): number }是一个调用签名call signature接口表示一个接受number参数并返回number的函数接口式调用签名的完整语法包括可选参数、rest 参数与重载写法见 可调用的。实例化之后idNum的类型被钉死为(x: number) numberidNum(42); // ok返回 number idNum(hello); // Error: string 不能赋值给 number 参数这一技巧的典型应用场景是将多态函数polymorphic function作为具体签名的参数传入某些 API 期望接收(x: number) number这类具体签名而你的工具函数是泛型实现的直接传参会因签名不匹配而报错此时实例化断言就是最轻量的桥接方式。这也是原文档灵感来源中讨论的实例化的多态函数作为参数问题的直接解法。泛型与断言的等价性辨析需要警惕的是泛型若只在签名中出现一次、不提供任何跨成员约束那么它本质上并不比类型断言更安全。泛型 中的误用泛型与方便通用两节对此有明确论述——例如declare function parseT(name: string): T;等价于declare function parse(name: string): any; const something parse(something) as TypeOfSomething;这与实例化思想一脉相承无论是用断言钉死泛型类还是用断言钉死泛型函数本质上都是把编译器无法验证的信任显式地交给开发者。相关类型工具从typeof到内置实例化类型要真正驾驭实例化技术还需要厘清类类型的两面性。在 常见问题类 中有一个关键结论定义一个类时实际上定义了两个不同的类型由类名推导出的实例类型如Foo描述实例的属性和方法构造函数本身的静态侧类型包含构造签名可用new调用以及所有静态成员可用typeof引用——即typeof Foo。这直接解释了为什么本主题的断言模式要写成Foo as { new (): Foonumber }而不是Foo as Foonumber前者针对的是构造函数侧静态侧后者针对的是实例侧。实例化操作的落点正是静态侧。TypeScript 还内置了两个基于infer的工具类型可以完成与实例化相反方向的类型提取详见 infer 关键字// 内置工具类型lib.d.ts 中的实现骨架 type ConstructorParametersT extends new (...args: any[]) any T extends new (...args: infer P) any ? P : never; type InstanceTypeT extends new (...args: any[]) any T extends new (...args: any[]) infer R ? R : any;用法示例class TestClass { constructor(public name: string, public age: number) {} } type Params ConstructorParameterstypeof TestClass; // [string, number] type Instance InstanceTypetypeof TestClass; // TestClass由此可见构造签名类型new (...args) T是这套类型体操的公共基础Foo as { new (): Foonumber }利用它做具体化实例化InstanceType则利用它做签名提取实例类型两者互为镜像。方案对比与选型建议维度构造签名断言as { new () ... }继承派生extends Foonumber函数签名断言as { (x) ... }是否创建新类否仅是类型视角的重塑是产生真正的子类否类型安全低编译器完全信任断言高类型关系由编译器推导低依赖开发者书写正确装饰器影响无基类装饰器不会作用于子类无典型场景快速导出/注入具体化类、.tsx 环境需要可扩展的具体化子类多态函数作为具体签名参数传入运行时开销零极小类声明本身零综合建议需要快、轻、零运行时时优先使用构造签名断言但务必配合单元测试或类型层面的冒烟用例弥补断言缺失的校验需要可扩展的具体化子类、或要规避断言风险时使用继承派生但先确认基类是否依赖装饰器面对泛型函数时用调用签名断言将其钉死为具体签名再安全地传给回调型 API。小结泛型的实例化类型是泛型与类型断言两大机制的交叉地带docs/tips/typeInstantiation.md 用最少的代码展示了它的全部要点——用as { new (): Foonumber }在构造函数侧为泛型类指定具体版本用继承获得更安全的派生版本用as { (x: number): number }为泛型函数钉死签名。理解这一模式不仅能在依赖注入、模块导出、回调参数等实战场景中少走弯路也能帮助你更深刻地体会 TypeScript结构性类型 编译时断言的设计哲学类型系统给出的安全边界需要开发者清楚地知道在哪里可以信任它在哪里必须自己负责。赞分享文档教程【免费下载链接】typescript-book-chineseTypeScript Deep Dive 中文版项目地址https://gitcode.com/gh_mirrors/ty/typescript-book-chinese点击查看免费下载相关推荐TypeScript 泛型详解从 Hello World 到泛型约束、泛型类与工厂函数的完整实践指南TypeScript 泛型详解从 Hello World 到泛型约束、泛型类与工厂函数的完整实践指南 泛型是 TypeScript 手册中文版中支撑组件可文档教程Go泛型实战理解多类型泛型参数的使用Go泛型实战理解多类型泛型参数的使用 本文是《Go泛型从入门到精通》系列教程的一部分将深入探讨Go语言中多类型泛型参数的使用方法。我们将通过一个实际案例展ImmersionBar代码混淆规则详解避免关键类被混淆ImmersionBar代码混淆规则详解避免关键类被混淆 在Android应用开发中代码混淆ProGuard/R8是保护代码安全和减小APK体积的重要手文档教程上一篇一次备份博主274条作品douyin-downloader 无水印批量下载与本地归档实战下一篇HsMod炉石 BepInEx 插件60 功能管 MMR、换肤与挂机创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表