TypeScript 入门第三部分 - 泛型及其他
仿制药
复杂类型
申报文件
声明合并
就是这样?
资源
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
这篇文章摘自我的博客,记得去看看博客上的最新内容哦😉
在这里,我们将继续TypeScript 入门教程。如果您还没看过第一部分和第二部分,请务必先阅读它们,以便快速上手 TypeScript,并了解本教程的内容。😉 在本文中,我们将最终探索泛型、一些复杂类型和声明文件。读完本系列教程后,您应该已经掌握了足够的 TypeScript 知识,可以编写非常复杂的代码了。那么,尽情享受吧! 🙂
仿制药
让我们从重要的内容开始!泛型(因为我们接下来要讨论的就是它)在 TypeScript 和其他一些支持泛型的静态类型语言中非常重要。但是,泛型究竟是什么呢?
可以合理推断,“generics”(通用)一词源于“general”(通用的),在此语境中意为“相同”。请看下面的函数。
function myFunction(arg: any): any {
return arg;
}
我们的函数接受任意类型的参数并直接返回它(我知道,这其实没什么用😅)。我们都知道,“任意类型”并不是类型安全的。它也没有表明返回类型与参数类型相同(虽然代码中可以读出这一点,但编译器却无法识别)。我们希望表明这些类型完全相同。不使用联合体、别名或其他任何特殊机制——绝对相同!这就是泛型发挥作用的地方。
function myGenericFunction<T>(arg: T): T {
return arg;
}
好了,这就是我们的通用函数……还有一些新的语法。😄 在<>类型参数声明部分之前,我们使用尖括号 ( ) 声明一个T类型(T是泛型类型最常用的名称,通常单字母比长名称更合适)。然后我们表明参数和返回值类型相同,但使用这个T类型。这才是真正的通用 😁,因为同一个变量类型在多个地方被使用。
但是它的T类型是什么呢?是 `int` string、number`int` 还是 `int` 等等?嗯,它可以是其中任何一种。调用泛型函数有两种方法。
myGenericFunction<string>('str');
第一种方法要求你直接指定实际类型,而不是使用 ` Ttype`。这里我们使用 `T` string。我们用类似的尖括号语法来表示这一点(这种语法在泛型中非常常见)。这样,必需参数的类型string以及返回值的类型都会变为 `T`。显然,这比使用`T` 或联合类型更好,也更安全。any
myGenericFunction(10);
第二种方法更常用,它利用了 TypeScript 类型推断和更具体的参数类型推断。这正是泛型的优势所在。我们T从10参数推断出的类型与参数的类型相同。这种选择之后会在所有使用类型number的地方得到体现。T
到目前为止,你应该对泛型有了相当不错的理解。但是,通过上面的例子,我知道你可能对它们的实用性有所怀疑。请相信我——你迟早会用到泛型(如果你用 TypeScript 编程的话😂),到那时你就会发现它们的潜力。尤其是在结合一些复杂类型(我们稍后会详细学习)或类型守卫时,泛型的作用更加显著。类型守卫能让你更充分地利用泛型。
另外,请记住函数中泛型类型的位置。它应该始终位于圆括号( ()) 之前,也就是参数部分之前。箭头函数也一样。更通用的做法是,将它们放置在之后调用时可以安全地添加尖括号的位置。你很可能会习惯这种做法。
通用世界
是的,泛型函数确实存在,但你知道泛型在整个 TypeScript 类型系统中都广泛应用吗?几乎在所有适用的地方都可以使用它们,尤其是在类和接口中。
class MyGenericClass<T, U> {
myProperty: T;
myProperty2: U;
constructor(arg: T) {
this.myProperty = arg;
}
}
如你所见,类与泛型配合使用效果非常好。就像在函数中一样,泛型类型可以在声明的上下文中的任何位置使用。我有没有提到你可以声明多个泛型类型?这适用于所有可以使用泛型的地方。只需用逗号 ( ,) 分隔泛型类型名称即可。
interface MyGenericInterface<T> {
myProperty: T;
myProperty2: T[];
}
上面是使用泛型连接接口的示例,看起来和使用类时一样。请注意,第二个属性是一个 T 类型的数组。我只是想再次展示 TypeScript 类型系统的所有组件是如何协同工作的。
由于类和接口与函数不同,因此不能使用参数类型推断来调用它们。你只能使用第一种方法——直接传递特定类型。否则,T 将等于一个空对象字面量。
interface MyGenericInterface<T> {
myProperty: T
}
class MyGenericClass <U> {
myProperty: MyGenericInterface<U>;
constructor(arg: U) {
this.myProperty = {
myProperty: arg
}
}
}
这个例子也展示了如何嵌套并更好地利用泛型。注意我们是如何将类泛型类型传递U给MyGenericInterface`in` 的myProperty。
另一个数组
最后,泛型部分还有一点需要补充。还记得我们之前使用特殊语法来指定数组类型吗?string[]其实还有另一种方法可以实现同样的效果。你可以使用内置的泛型数组接口,轻松实现相同的结果Array<string>。这是一种非常常见的做法。你可以在官方的TypeScript 标准库(所有 JavaScript 特性、Web API 等的类型定义/声明文件)以及其他一些流行的声明文件(我们稍后会介绍)中看到它,例如 React 的声明文件。
复杂类型
有了泛型,你就能体验到全新的可能性。现在我们可以探索一些类型,它们与泛型结合使用,能让你拥有更精细的控制。有了它们,你可以表达出非常有趣的结构。不过,现在也是时候去探索它们了!😎
扩展类型
你已经知道extends可以与类和接口一起使用的关键字。但在 TypeScript 中,它也有与泛型一起使用的用例。在这里,你可以使用它来限制/指定泛型类型应该继承自的类型。让我用一个例子来解释一下。
function myGenericFunction<T extends string>(arg: T): T {
return arg;
}
这里我们直接指定泛型类型应该继承自字符串类型。自然地,这很可能意味着它应该只是一个字符串string。但是,当你将类型指定为某种类时,它的派生类型也可以赋值。通常,这允许你更好地指定泛型类型及其应具有的属性,就像extends类和接口一样。
条件类型
条件类型是 TypeScript 类型系统中相当新的概念。它在 TypeScript v2.8中引入,允许你根据条件检查选择正确的类型。检查可以使用我们熟知的extends关键字和简单的语法来完成:
type MyType<T> = T extends string ? boolean : number;
上面我们定义了一个类型别名(也可以是泛型),并为其分配了一个条件类型。我们检查泛型类型 T 是否继承自字符串类型。如果是,则解析为布尔值;否则,解析为数字。当然,你也可以将此技巧应用于其他类型,以及嵌套多个 if 语句(反正它们都是类型 😉)。
索引类型
索引签名
我们之前已经讲解过如何在类、接口或对象字面量中声明属性。但是,如果想要创建一个包含任意数量键的对象,且每个键的类型都相同,该怎么办呢?当然,TypeScript 也提供了解决方案!😯
interface MyInterface {
[key: string]: number;
}
此功能称为索引签名,可用于接口、类和对象字面量。其语法由方括号 ( []) 组成,方括号内包含属性键的通用名称及其类型(通常为字符串,可选数字)。之后是属性值的类型。您可以将其理解为:每个属性(在本例中为字符串类型的键)都应该有一个数字类型的值。
请记住,TS 类型可以混合使用,因此您可以自由地使用索引签名,并结合可选指示符或默认值等技巧。此外,在创建除了索引签名之外还具有其他属性的结构时,请记住,这些属性也必须能够分配给已声明的签名!
关键
假设你有一个对象、接口或其他任何东西,并且想要创建一个函数,该函数接受对象的属性名作为参数并返回其值。当然,你可以直接将参数类型声明为字符串,但这样做不如使用字符串字面量联合体那样能获得 IDE 的充分支持。而这正是keyof运算符的用武之地。
const myObject = {
a: 1,
b: 2,
c: 3
}
function getProperty<T extends keyof (typeof myObject)>(propertyName: T): (typeof myObject)[T] {
return myObject[propertyName];
}
这里涉及到一些复杂的类型定义!花点时间自己分析一下。它基本上允许我们将参数明确地定义为联合类型,'a'|'b'|'c'并添加了真正具体的返回类型声明。
索引访问
在前面的例子中,你应该看到了返回类型使用了类似于 JavaScript方括号表示法的符号来访问对象属性。而我们在这里所做的也几乎完全相同,只不过这次是针对类型!
interface MyInterface {
myStringProperty: string
}
type MyString = MyInterface['myStringProperty'];
这里我们访问了myStringProperty`of`MyInterface并将其赋值给MyString类型别名,结果等于字符串。明白了吧?🚀
映射类型
映射类型顾名思义,允许将你的类型映射/转换为不同的形式。有了它们,你可以处理给定的类型,并以任何你想要的方式对其进行更改。
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
这里有一个实际的例子。我们的泛型Readonly类型接受T一个类型并对其进行转换,使每个属性都变为只读。语法类似于索引签名,但略有不同。我们不再使用标准的属性名及其类型对,而是使用一个in关键字。这允许我们遍历类型键的并集(类似于for...inT循环) ,从而定义P类型(字符串字面量)。一般来说,我们遍历 T 类型的属性并更改它们以创建一个新类型。就像.map()JavaScript 数组的方法一样。😉
申报文件
TypeScript 作为 JavaScript 的超集,可以轻松受益于 JS强大的生态系统和丰富的库。但类型推断并非万能。在这种情况下,它会使用任意类型,导致类型安全性降低。为了解决这个问题,TS 提供了创建类型声明文件(也称为typings)的选项。这些文件通常以.d.ts为扩展名,它们向 TS 编译器提供有关 JS 代码中类型的信息。这使得在 TS 中使用 JS 库能够保证高质量的类型安全性。
许多流行的 JS 库已经提供了自己的类型定义,它们要么打包在NPM包中,要么作为DefinitelyTyped仓库的一部分单独提供。但是,如果你选择的库没有类型定义文件,你可以根据该工具的文档和其他资源快速创建自己的类型定义。
创建自己的类型定义并不比编写 TypeScript 代码难多少,只是不涉及 JavaScript 部分,也就是说只涉及类型。此外,你通常需要declare在函数和变量前使用 `@type` 关键字来声明它们。官方 TypeScript 文档对此主题有很好的阐述,如果你感兴趣,可以去看看。
声明合并
声明合并是 TypeScript 中的一个重要概念,它允许你将给定结构的多个声明合并为一个。以下是一个合并两个相同接口声明的示例。
interface MyInterface {
myStringProperty: string;
}
interface MyInterface {
myNumberProperty: number;
}
最终生成的接口名称将MyInterface包含两个分别声明的属性。同样的方法也适用于其他一些 TypeScript 结构,例如类(部分适用)、枚举和命名空间。
模块扩充
当您需要在多个 JS 模块中扩展/更改给定值时,为了提供足够的类型安全性,您需要使用模块扩展。您可以使用declare module关键字对来实现这一点。
import MyClass from './classes';
declare module './classes` {
interface MyClass {
myBooleanProperty: boolean;
}
}
MyClass.prototype.myBooleanProperty = true;
就是这样?
本文几乎涵盖了编写专业 TypeScript 代码所需的一切。虽然还有一些其他特性,例如命名空间和mixin,但就我近两年的编码经验而言,我并不觉得它们有多必要,甚至觉得它们没什么用。
综上所述,我认为 TypeScript 入门教程到此结束。当然,如果您有兴趣,请务必阅读前两部分。或许您想在这个博客上看到更多关于 TypeScript 的内容?比如完整的TypeScript 配置文件概述,或者关于如何运用本系列所学知识的教程?请在评论区留言或分享您的想法。👏
和往常一样,欢迎在Twitter和Facebook上关注我,获取更多内容。也欢迎访问我的个人博客。🚀
资源
- TypeScript -来自“dotnetcurry.com”的泛型之旅;
- 迁移到 TypeScript:为来自“medium.com”的第三方 NPM 模块编写声明文件;
- 如何掌握来自“medium.freecodecamp.org”的高级TypeScript模式;