类型即正义:TypeScript 从入门到实践(四):5000字长文带你从新认识泛型

本文由图雀社区成员 pftom 写做而成,欢迎加入图雀社区,一块儿创做精彩的免费技术教程,予力编程行业发展。javascript

欢迎阅读 类型即正义,TypeScript 从入门到实践系列:html

本文所涉及的源代码都放在了 Github  或者 Gitee 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+GithubGitee仓库加星❤️哦~前端

此教程属于 React 前端工程师学习路线的一部分,欢迎来 Star 一波,鼓励咱们继续创做出更好的教程,持续更新中~java

在以前的文章中,咱们了解了 TypeScript 主要分为 JS 语言侧和类型侧两个部分。node

在介绍了类型侧的一些基础知识,咱们用这些学到的基础知识去注解对应的 JS 内容,将 JS 内容如变量、函数、类等类型化,这样确保写出的代码很是利于团队协做,且能快速排错。react

在了解了以前几篇文章里面的知识以后,你应该可使用 TypeScript 进行正常的项目开发了。git

源起

为何要学泛型?由于它能够帮助你 “面向编辑器代码提示编程” :)github

学习准备

配置 TypeScript 环境

建立一个 node 项目:typescript

mkdir ts-study
cd ts-study && npm init -y
复制代码

配置 TypeScript 环境:npm

npm install typescript # 安装 TypeScript
npx tsc --init # 生成 TypeScript 配置文件
复制代码

修改 tsconfig.json 文件,设置对应的 TS 编译器须要编译的文件以下:

{
  "compilerOptions": {
    "outDir": "./dist" // 设置编译输出的文件夹
  },
  "include": [
    // 须要编译的ts文件一个*表示文件匹配**表示忽略文件的深度问题
    "./src/**/*.ts"
  ],
  "exclude": ["node_modules", "dist", "**/*.test.ts"] // 排除不须要编译的文件夹
}
复制代码

配置 TypeScript 编译执行脚本,使用 VSCode 编辑器打开 ts-study 项目,而后修改 package.json 的 scripts 字段以下:

{
  "name": "ts-study",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsc",
    "build:w": "tsc -w"
  },
  "author": "pftom <1043269994@qq.com>",
  "license": "MIT",
  "dependencies": {
    "typescript": "^3.7.4"
  }
}
复制代码

接着在项目根目录新建 src 文件夹,并在里面新建  index.ts  文件,接着在项目根目录下的命令行执行 npm run build:w 开始监听 index.ts 文件的修改。

通过上面的操做,你的 VSCode 编辑器应该是以下样子:

image.png
其中 TERMINAL 终端表示正在监听文件修改并编译中,当前文件的编译结果没有错误,由于咱们的 src/index.ts 里面尚未写任何内容。一切准备就绪,开始 “面向编辑器代码提示编程”!✌️

从一个简单的例子提及

先不扯那么多泛型的概念,咱们先来看一个简单的例子,在 src/index.ts 里面编写以下代码:

function getTutureTutorialsInfo(info) {
  return info;
}
复制代码

咱们编写了一个获取图雀教程信息的函数,接收 info 输入,而后返回 info ,即明确参数类型和返回类型同样。如今这个还只是一个 JavaScript 函数,咱们来给它进行类型注解。

写一个 Low 一点的 TS 函数

.... 这怎么注解?此时正在阅读的你可能会冒出这个疑问。

对的,这怎么注解?咱们面临着以下几个难题:

  • info 类型未知,它多是 string 、 number 或者其余类型
  • info 类型未知的状况下,咱们还要注解返回值类型,而且此返回值类型要和 info 类型一致,因此咱们的返回值类型这里也未知。

相信有同窗会尝试这样去解决:

function getTutureTutorialsInfo(info: any): any {
  return info;
}
复制代码

很好!你成功写了第一个 "AnyScript` 函数,这个函数和 JS 函数无异,根本没法用到 TS 的强大类型补全机制,不信你能够在你的 VSCode 编辑器里面尝试加入以下代码:

function getTutureTutorialsInfo(info: any): any {
  console.log(info.length);
  return info;
}
复制代码

能够看到咱们添加了一个打印语句 console.log ,若是你没有 Copy 上面的代码,而是选择手敲的话,你会发现输入 info. 的时候,编辑器里面没有提示补全 length 属性,由于给 info 注解了 any 类型,TS 编译器没法推断此 info 是什么类型,因此也没有对应的补全,没有补全的 TypeScript 代码是没有生命的😿

类型的函数?

那么思考一下,这里该如何作类型注解了?相信你已经有答案了,这就是咱们这一节要引出的重点:“泛型” ,我将它称之为 “类型的函数”,对应 JS 的函数同样,声明一个 “类型变量”,而后在类型函数代码块里面可使用这个 “类型变量”。

一个 JS 函数以下:

function jsFunc(varOne) {
  const res = varOne + 1;
  return res;
}
复制代码

能够看到一个 JS 函数,有一个 varOne 参数,这个参数变量能够在函数体中使用。接下来咱们来看一下为何我把泛型称之为 “类型的函数”,修改咱们 src/index.ts 里面的内容:

function getTutureTutorialsInfo<T>(info: T): T {
  console.log(info.length);
  return info;
}
复制代码

能够看到咱们给 getTutureTutorialsInfo 后面加上 <T> 这个相似咱们上面那个 JS 函数的 (varOne) ,而后咱们在原 JS 函数参数和返回值中使用了这个 “类型变量”  T : (info: T): T ,这样咱们就解决了上面的两个难题:

  • 咱们定义了 T 这样一个 “类型变量”,并用它来注解咱们的 JS 函数参数 info 和其返回值,T 既然是一个 “类型变量”,那么接收此 “类型变量” 的 “类型的函数” - 泛型,在以后被调用的时候,咱们能够根据需求指定传入的类型,好比 string 、 number 等,这就确保了调用函数的用户来决定 info 的类型 T ,这样参数的类型就肯定了。
  • 参数和返回值类型都使用了 T 来作类型标注,因此参数值和返回值类型一致。

可是稍等,上面的代码在编辑器中报错了:

image.png
由于咱们将这个函数泛型化了,明确了泛型变量 T 是一个明确类型,因此咱们以前的 info.length 会报错,固然这里有同窗会问了,我要是这里 T 在以后泛型 (类型的函数)调用的时候传入的是 string 类型,那不是就有 length 属性了嘛,很遗憾,由于 T 还多是 number 类型,而 number 类型的变量没有 length 属性,因此 TS 编译器报错了。

为了解决上面的问题,咱们能够更近一步,对函数作出修改以下:

function getTutureTutorialsInfo<T>(info: T[]): T[] {
  console.log(info.length);
  return info;
}
复制代码

这样就好啦,不只类型肯定了,并且参数和返回值类型也一致,而且咱们还能明确的使用 info.length 了,由于 TS 编译器知道 info 是一个数组,这个时候你在 VSCode 编辑器里面输入 info. ,应该会提示你以下效果:

image.png
有了代码补全的 TS 充满了活力🔥!

通过上面的例子,咱们发现,其实泛型,就像是一个关于 “类型的函数” 同样,给定输入的类型变量,而后可使用输入变量通过组合好比 T[] 、进行联合类型或交叉类型操做,来做为注解类型使用。

类型函数的使用

上面咱们定义了第一个泛型 - “类型的函数”,接下来咱们来尝试使用咱们的泛型,在 src/index.ts 中对代码作出对应的修改以下:

function getTutureTutorialsInfo<T>(info: T[]): T[] {
  console.log(info.length);
  return info;
}

getTutureTutorialsInfo<string>(['hello tuture', 'hello world'])
复制代码

能够看到对应 <T> 定义了泛型中的类型变量,咱们在调用泛型时,也对应写了 <string> ,这样 T 就在 getTutureTutorialsInfo 函数中就会以 string 的类型被使用,参数 info 和返回值也就对应了 string[] ,你的 VSCode 编辑器里面调用的效果应该以下图,将鼠标移动到 getTutureTutorialsInfo 上,会直接显示 getTutureTutorialsInfo 函数的类型定义,能够看到已经成功将 T 换成了 string 。

image.png

而且咱们还了解到,泛型的使用和 JS 函数的调用一脉相承,更加坚决了咱们 泛型 就是 “类型的函数” 的说法和认知。

注意:

  • 上面的泛型中使用的 T 变量,其实只是一个 TypeScript 界比较习惯性的用法,经常使用的还有 U 等,固然你也能够写成 YourT ,这里不限制。
  • 上面的泛型调用时,T 接受的类型能够是任意类型,好比对象、函数等类型,不只仅限于 string 、 number 等

泛型,再回顾

咱们在上面用了不少的笔墨来试图将泛型和 “类型的函数” 划上等号,目的是为了让你理解泛型它工做的一个原本面貌。了解了泛型原本面貌以后,相信诸如使用泛型可使得 TS 代码组件化,复用代码,你也能了然如胸了。

泛型是在调用时再限定类型

咱们在定义泛型的时候,是一系列类型变量,如 T 、 U 等,这些变量实际的类型咱们在定义的时候是不知道的,只有在进行泛型调用的时候,由用户给定实际的类型,因此这里有一种延迟声明类型的做用。

泛型是否也有多个类型变量?

那么,既然泛型能够看作是 “类型的函数”,那么函数能接收多个参数的话,咱们的泛型也能接收多个类型变量,好比:

function getTutureTutorialsInfo<T, U>(info: T[], profile: U): T[] {
  console.log(info.length);
  console.log(profile);
  return info;
}

getTutureTutorialsInfo<string, object>(['hello tuture'], { username: 'tuture'})
复制代码

能够看到,咱们修改了 getTutureTutorialsInfo 函数的泛型定义,添加了一个新的类型变量 U ,并用 U 来注解了函数的第二个参数 profile 的类型。

一样,在调用 getTutureTutorialsInfo 函数的时候,咱们也须要传入两个类型变量,这里咱们的 profile 被认为是一个 object 类型。

匿名函数泛型?

在以前的内容中,咱们经过命名函数来说解了泛型,那么匿名函数如何使用泛型了?其实和命名函数相似,只不过匿名函数是以下形式:

const getTutureTutorialsInfo: <T>(info: T[]) => T[] = (info) => {
  console.log(info.length);
  return info;
}

// 或者
const getTutureTutorialsInfo: <T>(info: T[]) => T[] = function (info) {
  console.log(info.length);
  return info;
}
复制代码

咱们直接给匿名函数被赋值的变量进行匿名函数的注解,并加上泛型,你应该回想起以前给一个变量注解函数类型时的样子:

(args1: type1, args2: type2, ..., args3: type3) => returnType
复制代码

而匿名函数泛型只不过在以前加上了 <T> 类型变量,而后能够用于注解参数和返回值。

泛型默认类型参数?

既然咱们声称泛型是关于 “类型的函数”,为了更加深入的论证咱们这个观点,咱们再进一步。

咱们都知道函数存在默认参数一说,那么做为 “类型的函数” - 泛型,是否也有默认类型参数这一说了?很差意思,还真的有!咱们来看个例子:

function getTutureTutorialsInfo<T, U = number>(info: T[], profile: U): T[] {
  console.log(info.length);
  console.log(profile);
  return info;
}

getTutureTutorialsInfo<string, string>(['hello world'], 'hello tuture')
复制代码

能够看到咱们给类型变量 U 一个默认的类型参数 number (还记得 ES6 里面有默认值的参数必须靠后放置嘛?)

以后咱们在进行泛型调用的时候,却给 U 传了 string 类型,把这段代码放到 src/index.ts 里面,应该不会报错,而且编辑器里面有良好的提示:

image.png

泛型,继续前进

接下来咱们继续深刻泛型,解答以前文章里的一些疑问,好比:

  • 泛型数组
  • 类泛型

同时咱们还会了解一些新的概念,好比:

  • 接口泛型
  • 类型别名泛型
  • 泛型约束

解决遗留的问题

泛型数组

这个咱们已经在上面的例子中用到了,泛型实际上定义了一系列类型变量,而后咱们能够对这些类型变量作任意的组合以适应各类不一样的类型注解需求,其中一个组合例子就是泛型数组 - 某个类型变量的数组形态,也就是咱们上面提到的 info: T[] ,其中 T[] 就是泛型数组。

固然泛型数组的表达形式还有另一种:

Array<T> 
复制代码

即以泛型调用的形式返回一个关于泛型变量 T 的数组类型。因此咱们的 getTutureTutorialsInfo 函数能够写成以下样子:

function getTutureTutorialsInfo<T>(info: Array<T>): Array<T> {
  console.log(info.length);
  return info;
}

getTutureTutorialsInfo<string>(['hello tuture', 'hello world'])
复制代码

类泛型

类泛型的形式和函数泛型相似,咱们来看一个类泛型的定义的调用,在 src/index.ts 里面额外添加下面的内容:

// 上面是 getTutureTutorialsInfo 泛型函数的定义和调用

class Tuture<T> {
 	info: T[];
}

let tutorial = new Tuture<string>()
tutorial.info = ['hello world', 'hello tuture'];
复制代码

类泛型的定义也是在类名以后添加 <T> 这样的形式,而后就能够在类中使用 T 类型变量来注解类型。而类泛型的调用和函数泛型的调用相似。

学习了类泛型,咱们再来解析一下在上一篇文章中提到的那个 TodoInput 组件,相似下面这样:

class TodoInput extends React.Component<TodoInputProps, TodoInputState> {
  // ... 组件内容 
}
复制代码

这个实际上分为两个部分,首先是 React.Component 组件基类的类泛型调用,而后是 TodoInput 集成自这个类泛型。由于派生类 TodoInput 能够获取到父类的属性和方法,因此在 TodoInput 中使用的 this.props 和 this.state 在被类型注解以后,就能够在编码时自动补全,你在写代码的时候应该能够享受到以下好处:

image.png

开启新篇章

了解了函数泛型、类泛型,你有可能有一点想法了关于泛型,是否是咱们以前的不少讲解过的内容,如类型别名、接口等。你想对了!TS 会在尽量多的地方,能用泛型就用上泛型,由于泛型能够将代码组件化,方便复用,全部智能的编译器,能不让你多写的东西,就绝对不会让你多写,统统用泛型给整上。

接口泛型

在了解接口泛型以前,咱们先来看一个接口是怎么写的,在 src/index.ts 里面添加以下代码:

interface Profile {
  username: string;
  nickName: string;
  avatar: string;
  age: string;
}
复制代码

通常咱们的 Profile 相似上面的内容,可是有时候有些字段会根据需求的不一样而不一样,好比 age 这个字段,有些人喜欢定义成数字类型 number ,有些人喜欢定义成字符串类型 string ,因此这又是一个延迟赋予类型的例子,能够借助泛型来解决,咱们修改一下上面的代码:

 interface Profile<T> {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

type ProfileWithAge = Profile<string>
复制代码

能够看到,接口泛型的声明和调用与函数、类泛型的相似,它容许你在接口里面定义一些属性,使用类型变量来注解,在调用时指明这个属性的类型。

类型别名泛型

由于在不少场景下,类型别名和接口充当相似的角色,因此在了解完接口泛型以后,咱们有必要来了解学习一下类型别名如何结合泛型使用,和接口相似,将上面的接口泛型 Profile 用类型别名重写以下:

type Profile<T> = {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

type ProfileWithAge = Profile<string>
复制代码

能够看到,基本一致!

泛型约束

咱们来解决以前的一个遗留问题,那就是即便我使用了泛型,我仍是不知道某个被泛型的类型变量注解的变量的一个结构是怎么样的即:

function getTutureTutorialsInfo<T, U>(info: T[], profile: U): T[] {
  console.log(info.length);
  console.log(profile);
  return info;
}

getTutureTutorialsInfo<string, object>(['hello tuture'], { username: 'tuture'})
复制代码

上面咱们用类型变量 U 注解了 profile 参数,但咱们在使用 profile 的时候,依然不知道它是什么类型,也就是说泛型虽然解决了类型的可复用性,可是仍是不能让咱们写代码时得到自动补全的能力😭

重申:没有补全的 TypeScript 代码是没有生命的!

那么咱们如何让在既使用泛型的同时,还能得到代码补全了?答案相信你也猜到了, 那就是咱们这一节要讲的泛型约束。 咱们修改 src/index.ts  里面的代码以下:

type Profile<T> = {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

function getTutureTutorialsInfo<T, U extends Profile<string>>(info: T[], profile: U): T[] {
  console.log(info.length);
  console.log(profile);
  return info;
}

复制代码

能够看到,咱们复用了以前定义的 getTutureTutorialsInfo 和 Profile ,可是在 getTutureTutorialsInfo 泛型中第二个类型变量作了点改进,以前只是单纯的 U ,如今是 U extends Profile<string> , Profile<string> 表示调用类型别名泛型生成一个 age 为 string 的新类型别名,而后经过 U extends ... 的方式,用 Profile<string> 来限制 U 的类型,也就是 U 必须至少包含 Profile<string> 的类型。

这个时候,咱们在 VSCode 编辑器里面尝试输入 profile. ,应该能够神奇的发现,有了自动补全:

image.png
而且还能了解到 age 是 string 属性!

再次!有了代码补全的 TS 充满了活力🔥!

固然这里的用于约束的 Profile<string> 能够是一个类型别名,也能够是一个接口,也能够是一个类:

class Profile<T>  {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

// 或者
interface Profile<T>  {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

// 或者
type Profile<T> = {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}
复制代码

更近一步,这里的用于约束类型变量的类型能够是一些更加高级的类型如联合类型、交叉类型等:

type Profile<T> = {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

type Tuture = {
	github: string;
  remote: string[];
}

function getTutureTutorialsInfo<T, U extends Profile<string> & Tuture>(info: T[], profile: U): T[] {
  console.log(info.length);
  console.log(profile);
  return info;
}

复制代码

能够看到咱们使用了 Profile<string> 和 Tuture 的交叉类型来约束 U ,在咱们的 VSCode 编辑器里面应该会有以下补全效果:

image.png

深刻实践,注解构造函数

在了解泛型的基础知识,而且结合函数、接口、类型别名和类进行结合使用以后,相信你对如何使用泛型已经有了一点经验了。

而了解了泛型,你就能够开始尝试深刻 TS 类型编程的世界了!接下来咱们开始深刻一下高阶的 TS 类型编程知识,并尝试讲解一些比较边缘状况如何进行类型注解。

咱们须要一个 createInstance 函数,它接收一个类构造函数,而后返回此类的实例,并能在调用以后得到良好的代码补全提示(!很重要),而且此函数还须要有足够好的通用性能处理任意构造函数(!泛型) 。咱们尝试在 src/index.ts  里面编写一个类以及一个建立此类实例的方法:

class Profile<T> {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

class TutureProfile extends Profile<string> {
	github: string; 
  remote: string[];
}

function createInstance(B) {
  return new B();
}

const myTutureProfile = createInstance(TutureProfile);
复制代码

不要问我为何 createInstance 的参数是 B ,由于咱们最后很 new B() 。😁

当咱们编写了上面这个 createInstance 时,当咱们尝试在调用以后输入 . : createInstance(TutureProfile). ,发现编辑器里面没有补全提示实例化对象的相关属性如 username 等

image.png
首先咱们来解析一下构造函数的样子,由于 TS 类型是鸭子类型,是基于代码的实际样子来进行类型注解的。构造函数是可被实例化的函数,便可以经过 new XXX() 进行调用来建立一个实例,因此构造函数的注解应该相似这样:

interface ConstructorFunction<C> {
 	 new (): C;
}
复制代码

即形如 new (): C 的函数形式,表示能够经过调用 new XXX() 生成一个 XXX 的实例。即某个类:

class Profile<T> {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}
复制代码

咱们注解其构造函数相似下面:

const profileConstructor: ConstructorFunction<Profile<string>> = Profile;
复制代码

这里有同窗还记得嘛,咱们在上一篇文章中讲到一个类在声明的时候会声明两个东西:1)用于注解此类实例的类型 2)以及此类的构造函数。这个例子是用来表达类在声明时声明的这两样东西的最佳例子之一即:

  • ConstructorFunction 接口泛型接收的 C 用来注解 new () 生成的实例,此为第一:用于注解此类实例的类型。
  • 用于注解 Profile 的构造函数的类型 ConstructorFunction<Profile<string>> ,在注解 profileConstructor 变量以后,其初始化赋值是 Profile 自己,而且你能够在你的 VSCode 编辑器里面编写上面的代码,应该不会报错,这说明了第二:声明了此类的构造函数。

了解了构造函数如何进行类型注解以后,咱们来完成第三点要求,让这个 createInstance 更具通用性,二话不说,泛型走起!最终代码以下:

class Profile<T> {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

class TutureProfile extends Profile<string> {
	github: string; 
  remote: string[];
}

interface ConstructorFunction<C> {
 	 new (): C;
}


function createInstance<A extends Profile<string>>(B: ConstructorFunction<A>) {
  return new B();
}

const myTutureProfile = createInstance(TutureProfile);
复制代码

如今你在 VSCode 编辑器 createInstance(TutureProfile) 后面输入 . 应该能够看到代码补全:

image.png
这个例子其实关于 extends 类型约束那一块有点多余,可是为了组合咱们在这一篇里面学到的知识,因此我额外把它也加上了,能够看到咱们重拾了全部的代码补全,代码补全🐂🍺

上面类中如 remote 等属性会有红色下划线是由于报了 Property 'remote' has no initializer and is not definitely assigned in the constructor.ts(2564) ,字面意思就是没有初始化这些属性,这个不重要,能够经过配置移除,也能够初始化。It's your choice!

参考资料

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

相关文章
相关标签/搜索