文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载泛型是 TypeScript 手册中文版中支撑组件可重用性的核心机制它让开发者能编写既支持当前数据类型、又兼容未来数据类型的 API是构建大型系统的关键能力。本文以 zh/handbook/generics.md 为主干从泛型函数的定义、类型变量、泛型接口与泛型类到泛型约束、类型参数互约束与类类型工厂函数层层展开并结合本文档对应的 v2 版 zh/handbook-v2/type-manipulation/generics.md 及仓库内 zh/handbook/classes.md 等配套章节帮助你完整掌握类型安全的可重用组件设计并能直接落地到实际项目中。为什么需要泛型可重用性与类型安全的平衡软件工程中我们不仅要创建一致、定义良好的 API同时也要考虑可重用性。组件不仅能够支持当前的数据类型同时也能支持未来的数据类型——这在创建大型系统时为你提供了十分灵活的功能。在像 C# 和 Java 这样的语言中可以使用泛型Generics来创建可重用的组件一个组件可以支持多种类型的数据这样用户就可以以自己的数据类型来使用组件。TypeScript 的泛型与此同源。它的核心价值在于既能写出只写一次的通用逻辑又能在编译期保留参数与返回值之间的类型关系而不是像any那样把所有类型信息“打回原形”。泛型之 Hello World从 identity 函数理解类型变量要理解泛型最好的起点是经典的 identity 函数——它原样返回传入的任何值你可以把这个函数当成是echo命令。不用泛型的话这个函数可能是下面这样只能处理numberfunction identity(arg: number): number { return arg; }或者我们使用any类型来定义函数function identity(arg: any): any { return arg; }使用any类型会导致这个函数可以接收任何类型的arg参数这样就丢失了一些信息传入的类型与返回的类型应该是相同的。如果我们传入一个数字我们只知道任何类型的值都有可能被返回——函数看似灵活实则丧失了类型保证。因此我们需要一种方法使返回值的类型与传入参数的类型是相同的。这里我们使用了类型变量Type Variable它是一种特殊的变量只用于表示类型而不是值function identityT(arg: T): T { return arg; }我们给 identity 添加了类型变量T。T帮助我们捕获用户传入的类型比如number之后我们就可以使用这个类型。之后我们再次使用了T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。这允许我们跟踪函数里使用的类型的信息。我们把这个版本的identity函数叫做泛型因为它可以适用于多个类型。不同于使用any它不会丢失信息像第一个例子那样保持准确性——传入数值类型并返回数值类型。从这个角度看泛型同时具备了两个世界中最好的特性any的普适性以及明确标注类型的精确性。关于类型变量的命名文档中使用T作为约定俗成的占位名取自 Template 或 Type。在 v2 版手册中则常用Type、Input等更具可读性的名称。只要类型参数的数量和使用方式对应名字并不重要这一点在后续泛型类型一节中会再次体现。泛型函数的两种调用方式我们定义了泛型函数后可以用两种方法使用。第一种是显式传入类型参数包含所有的参数let output identitystring(myString); // type of output will be string这里我们明确的指定了T是string类型并做为参数传给函数使用了括起来而不是()。第二种方法更普遍利用类型推论Type Inference——编译器会根据传入的参数自动地帮助我们确定 T 的类型let output identity(myString); // type of output will be string注意我们没必要使用尖括号来明确地传入类型编译器可以查看myString的值然后把T设置为它的类型。类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话只能像上面那样明确的传入 T 的类型——在一些复杂的情况下例如参数被包裹在更深层的结构里、或类型信息被any污染这是可能出现的。使用泛型变量把类型参数当作任意或所有类型对待使用泛型创建像identity这样的泛型函数时编译器要求你在函数体必须正确的使用这个通用的类型。换句话说你必须把这些参数当做是任意或所有类型——因为在函数内部你并不知道调用方最终会传入哪种具体类型。看下之前identity例子function identityT(arg: T): T { return arg; }如果我们想同时打印出arg的长度很可能会这样做function loggingIdentityT(arg: T): T { console.log(arg.length); // Error: T doesnt have .length return arg; }如果这么做编译器会报错说我们使用了arg的.length属性但是没有地方指明arg具有这个属性。记住这些类型变量代表的是任意类型所以使用这个函数的人可能传入的是个数字而数字是没有.length属性的。这正是类型变量代表任意类型的直接推论——编译器不允许你对一个未约束的类型做任何属性访问假设。解决思路把 T 作为类型的一部分而非整个类型现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组所以.length属性是应该存在的。我们可以像创建其它数组一样创建这个数组function loggingIdentityT(arg: T[]): T[] { console.log(arg.length); // Array has a .length, so no more error return arg; }你可以这样理解loggingIdentity的类型泛型函数loggingIdentity接收类型参数T和参数arg——arg是个元素类型是T的数组并返回元素类型是T的数组。如果我们传入数字数组将返回一个数字数组因为此时T的类型为number。这可以让我们把泛型变量 T当做类型的一部分使用而不是整个类型从而增加灵活性。我们也可以这样实现上面的例子使用更接近 Java/C# 习惯的ArrayT语法function loggingIdentityT(arg: ArrayT): ArrayT { console.log(arg.length); // Array has a .length, so no more error return arg; }使用过其它语言的话你可能对这种语法已经很熟悉了。在下一节会介绍如何创建自定义泛型类型让它像内置的ArrayT一样工作。泛型类型函数签名、对象字面量与泛型接口上一节我们创建了 identity 通用函数可以适用于不同的类型。在这节我们研究一下函数本身的类型以及如何创建泛型接口。泛型函数的类型与非泛型函数的类型没什么不同只是有一个类型参数在最前面像函数声明一样function identityT(arg: T): T { return arg; } let myIdentity: T(arg: T) T identity;类型参数名可以不同数量与用法对应即可我们也可以使用不同的泛型参数名只要在数量上和使用方式上能对应上就可以function identityT(arg: T): T { return arg; } let myIdentity: U(arg: U) U identity;这里类型签名中的U与函数定义中的T只是名字不同其结构一个类型参数、参数与返回值同型完全一致因此赋值是合法的。这印证了前文所说的泛型参数的名称只是约定结构性一致才是关键。用对象字面量的调用签名定义泛型函数我们还可以使用带有调用签名的对象字面量来定义泛型函数function identityT(arg: T): T { return arg; } let myIdentity: { T(arg: T): T } identity;这引导我们去写第一个泛型接口了。我们把上面例子里的对象字面量拿出来做为一个接口interface GenericIdentityFn { T(arg: T): T; } function identityT(arg: T): T { return arg; } let myIdentity: GenericIdentityFn identity;把类型参数放在接口上让所有成员共享类型一个相似的例子我们可能想把泛型参数当作整个接口的一个参数。这样我们就能清楚的知道使用的具体是哪个泛型类型比如Dictionarystring而不只是Dictionary。这样接口里的其它成员也能知道这个参数的类型了interface GenericIdentityFnT { (arg: T): T; } function identityT(arg: T): T { return arg; } let myIdentity: GenericIdentityFnnumber identity;注意我们的示例做了少许改动。不再描述泛型函数而是把非泛型函数签名作为泛型类型的一部分。当我们使用GenericIdentityFn的时候还得传入一个类型参数来指定泛型类型这里是number锁定了之后代码里使用的类型。对于描述哪部分类型属于泛型部分来说理解何时把参数放在调用签名里泛型属于这个具体函数和何时放在接口上泛型属于整个接口所有成员共享是很有帮助的——这是设计 API 时的一个重要取舍。除了泛型接口我们还可以创建泛型类。注意无法创建泛型枚举和泛型命名空间——这是 TypeScript 的语法边界。泛型类实例部分可用静态部分不可用泛型类看上去与泛型接口差不多。泛型类使用括起泛型类型跟在类名后面class GenericNumberT { zeroValue: T; add: (x: T, y: T) T; } let myGenericNumber new GenericNumbernumber(); myGenericNumber.zeroValue 0; myGenericNumber.add function (x, y) { return x y; };GenericNumber类的使用是十分直观的并且你可能已经注意到了没有什么去限制它只能使用number类型。也可以使用字符串或其它更复杂的类型let stringNumeric new GenericNumberstring(); stringNumeric.zeroValue ; stringNumeric.add function (x, y) { return x y; }; console.log(stringNumeric.add(stringNumeric.zeroValue, test));与接口一样直接把泛型类型放在类后面可以帮助我们确认类的所有属性都在使用相同的类型——zeroValue与add的参数、返回值被锁定为同一个T。泛型类与静态部分的边界我们在类那节说过类有两部分静态部分和实例部分在 classes.md 中明确说明类具有实例部分与静态部分let Greeter被赋值为构造函数这个构造函数也包含了类的所有静态属性。泛型类指的是实例部分的类型所以类的静态属性不能使用这个泛型类型——因为静态属性属于构造函数本身而构造函数的类型在类声明时尚未绑定任何具体的类型参数。换句话说类型参数只有等你new GenericNumbernumber()之后才被实例化绑定而静态成员在类被创建的那一刻就已确定二者在时间上无法匹配。泛型约束用 extends 限定类型参数的能力边界你应该会记得之前的一个例子我们有时候想操作某类型的一组值并且我们知道这组值具有什么样的属性。在loggingIdentity例子中我们想访问arg的length属性但是编译器并不能证明每种类型都有length属性所以就报错了function loggingIdentityT(arg: T): T { console.log(arg.length); // Error: T doesnt have .length return arg; }相比于操作 any 所有类型我们想要限制函数去处理任意带有.length属性的所有类型。只要传入的类型有这个属性我们就允许——就是说至少包含这一属性。为此我们需要列出对于 T 的约束要求。为此我们定义一个接口来描述约束条件。创建一个包含.length属性的接口使用这个接口和extends关键字来实现约束interface Lengthwise { length: number; } function loggingIdentityT extends Lengthwise(arg: T): T { console.log(arg.length); // Now we know it has a .length property, so no more error return arg; }现在这个泛型函数被定义了约束因此它不再是适用于任意类型loggingIdentity(3); // Error, number doesnt have a .length property我们需要传入符合约束类型的值必须包含必须的属性loggingIdentity({ length: 10, value: 3 });{ length: 10, value: 3 }之所以合法是因为 TypeScript 采用结构性子类型化参见 interfaces.md 中只要传入的对象满足必要条件那么它就是被允许的的论述——对象只要包含length: number即可通过检查多出来的value属性并不会导致错误。在泛型约束中使用类型参数K extends keyof T你可以声明一个类型参数且它被另一个类型参数所约束。比如现在我们想要用属性名从对象里获取这个属性并且我们想要确保这个属性存在于对象obj上因此我们需要在这两个类型之间使用约束function getPropertyT, K extends keyof T(obj: T, key: K) { return obj[key]; } let x { a: 1, b: 2, c: 3, d: 4 }; getProperty(x, a); // okay getProperty(x, m); // error: Argument of type m isnt assignable to a | b | c | d.这里keyof T会求出T的所有键组成的联合类型对上述x而言即a | b | c | d而K extends keyof T将第二个类型参数锁定在这个联合类型之内。于是getProperty(x, a)合法而getProperty(x, m)在编译期就被拒绝——这实际上为类型安全的属性访问提供了编译期保障是keyof与泛型约束组合的最典型用法。在泛型里使用类类型构造函数签名的工厂函数在 TypeScript 使用泛型创建工厂函数时需要引用构造函数的类类型。比如function createT(c: { new (): T }): T { return new c(); }参数c: { new (): T }是一个构造函数签名构造签名表示一个可以用new调用、并产出T类型实例的构造函数。这样create就能安全地实例化并返回类型为T的对象。一个更高级的例子使用原型属性推断并约束构造函数与类实例的关系class BeeKeeper { hasMask: boolean; } class ZooKeeper { nametag: string; } class Animal { numLegs: number; } class Bee extends Animal { keeper: BeeKeeper; } class Lion extends Animal { keeper: ZooKeeper; } function createInstanceA extends Animal(c: new () A): A { return new c(); } createInstance(Lion).keeper.nametag; // typechecks! createInstance(Bee).keeper.hasMask; // typechecks!这里的逻辑链是createInstance通过A extends Animal约束实例类型必须是Animal的子类构造函数签名new () A让传入的类构造器与返回类型A绑定于是createInstance(Lion)的返回值被推断为Lion因此可以直接访问keeper.nametag——类型信息沿着继承链被完整保留。这种以构造函数类型为媒介的工厂模式在依赖注入、对象池、ORM 等场景中非常实用v2 版手册还指出这种模式被用于实现 mixins 设计模式见 zh/handbook-v2/type-manipulation/generics.md。进阶泛型参数默认值v2 手册补充在 v2 版手册 zh/handbook-v2/type-manipulation/generics.md 中还补充了泛型参数默认值这一能力通过为泛型参数声明默认值可以选择性地指定相应的类型参数。例如一个创建新的HTMLElement的函数。不带参数调用时生成HTMLDivElement将元素作为第一个参数调用时生成与参数类型相对应的元素还可以选择性地传递子元素列表。以前你必须通过重载将函数定义为type ContainerT, U { element: T; children: U; }; declare function create(): ContainerHTMLDivElement, HTMLDivElement[]; declare function createT extends HTMLElement(element: T): ContainerT, T[]; declare function createT extends HTMLElement, U extends HTMLElement( element: T, children: U[] ): ContainerT, U[];有了泛型参数默认值我们可以将其简化为单个声明type ContainerT, U { element: T; children: U; }; declare function createT extends HTMLElement HTMLDivElement, U T[]( element?: T, children?: U ): ContainerT, U; const div create(); // ContainerHTMLDivElement, HTMLDivElement[] const p create(new HTMLParagraphElement()); // ContainerHTMLParagraphElement, ...泛型参数默认值遵循以下规则摘自 v2 手册可直接作为实现参考如果类型参数有默认值则被视为可选的必需的类型参数不能在可选的类型参数之后类型参数的默认类型必须满足类型参数的约束如果存在在指定类型参数时只需要为必需的类型参数指定类型参数未指定的类型参数将解析为其默认类型如果指定了默认类型并且无法推断出候选项则会推断为默认类型与现有类或接口声明合并的类或接口声明可以引入对现有类型参数的默认值与现有类或接口声明合并的类或接口声明可以引入新的类型参数只要它指定了默认值。完整能力地图与速查表把本文全部知识点汇总如下方便速查与复习主题语法要点核心语义泛型函数function identityT(arg: T): T捕获参数类型并用于返回类型不丢失类型信息类型变量T或Type、U等任意名字只表示类型不表示值的特殊变量调用方式identitystring(...)显式 /identity(...)类型推论编译器自动推断T类型参数作为部分类型arg: T[]/arg: ArrayT允许访问数组.length提升灵活性泛型函数类型let myIdentity: T(arg: T) T类型参数写在调用签名最前面泛型接口interface GenericIdentityFnT { (arg: T): T }类型参数放接口上所有成员共享泛型类class GenericNumberT仅实例部分可用静态属性不可用泛型约束T extends Lengthwise限制 T 必须满足结构约束类型参数互约束K extends keyof T把 K 锁定在 T 的键联合类型内类类型工厂c: { new (): T }/new () A引用构造函数类型安全实例化泛型参数默认值T extends HTMLElement HTMLDivElement类型参数可省略解析为默认类型使用建议当你在实际项目中编写通用工具函数如缓存、防抖、深拷贝、数据校验器时优先使用泛型 约束的组合当约束条件依赖对象自身的键时使用keyof T当需要把类本身当作参数传递时使用构造签名new () T。这套能力覆盖了从类型安全的 echo到可复用的依赖注入工厂的完整跨度是 TypeScript 类型系统中最值得熟练掌握的一环。如需与相关主题联动阅读可参考仓库中的 泛型v2、接口、类 与 函数 各章节。赞分享文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载相关推荐RedwoodJS泛型编程泛型约束与类型推断完整指南RedwoodJS泛型编程泛型约束与类型推断完整指南 RedwoodJS作为现代全栈应用框架深度集成了TypeScript的泛型编程能力为开发者提供了强大后端前端Web框架开发工具The Concise TypeScript Book 泛型精读从类型参数、泛型类到约束与上下文收窄The Concise TypeScript Book 泛型精读从类型参数、泛型类到约束与上下文收窄 泛型Generics是 TypeScript 构建可文档教程CangjieCorpus仓颉泛型终极指南从泛型函数到泛型约束的8大核心概念CangjieCorpus仓颉泛型终极指南从泛型函数到泛型约束的8大核心概念 本文基于 CangjieCorpus仓颉编程语言语料库 的官方开发指南带你文档知识库教程OpenHarmony上一篇MongoDB GenAI Cookbook实战构建企业级RAG应用的完整流程下一篇最速上手用sqlx构建可再生能源技术评估工具的数据库层创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考