十分钟教你理解TypeScript中的泛型

转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。html

139239-20190711145116037-11212706.png

你将在本文中学到什么

本文介绍TypeScript中泛型(Generics)的概念和用法,它为何重要,及其使用场景。咱们会以一些清晰的例子,介绍其语法,类型和如何构建参数。你能够在你的集成开发环境中跟着实践。前端

准备工做

要从本文中跟着学习的话,你须要在电脑上准备如下东西:node

安装Node.js:你能够运行命令行检查Node是否安装好了。typescript

node -v

安装Node Package Manager: 一般安装Node时,它会顺带安装好所需版本的NPM。
安装TypeScript:若是你安装好了Node Package Manager,你能够用如下命令在本机的全局环境安装TypeScript。shell

npm install -g typescript

集成开发环境:本文将使用微软团队开发的Visual Studio Code。能够在这里下载。进入其下载的目录,并按照提示进行安装。记得选择“添加打开代码”(Add open with code)选项,这样你就能够在本机从任何位置轻松打开VS Code了。
本文是写给各层次的TypeScript开发人员的,包括但并不仅是初学者。 这里给出了设置工做环境的步骤,是为了照顾那些TypeScript和Visual Studio Code的新手们。npm

TypeScript里的泛型是个啥

在TypeScript中,泛型是一种建立可复用代码组件的工具。这种组件不仅能被一种类型使用,而是能被多种类型复用。相似于参数的做用,泛型是一种用以加强类(classes)、类型(types)和接口(interfaces)能力的很是可靠的手段。这样,咱们开发者,就能够轻松地将那些可复用的代码组件,适用于各类输入。然而,不要把TypeScript中的泛型错当成any类型来使用——你会在后面看到这二者的不一样。json

相似C#和Java这种语言,在它们的工具箱里,泛型是建立可复用代码组件的主要手段之一。即,用于建立一个适用于多种类型的代码组件。这容许用户以他们本身的类使用该泛型组件。数组

在VS Code中配置TypeScript

在计算机中建立一个新文件夹,而后使用VS Code 打开它(若是你跟着从头开始操做,那你已经安装好了)。安全

在VS Code中,建立一个app.ts文件。个人TypeScript代码都会放在这里面。app

把下面打日志的代码拷贝到编辑器中:

console.log("hello TypeScript");

按下F5键,你会看到一个像这样的launch.json文件:

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "TypeScript",
      "program": "${workspaceFolder}\\app.ts",
      "outFiles": [
        "${workspaceFolder}/**/*.js"
      ]
    }
  ]
}

里面的name字段的值,原本是Launch Program,我把它改为了TypeScript。你能够把它改为其余值。

点击Terminal Tab,选择Run Tasks,再选择一个Task Runner:"TypeScript Watch Mode",而后会弹出一个tasks.json文件,把它改为下面像这样:

{
 // See https://go.microsoft.com/fwlink/?LinkId=733558
 // for the documentation about the tasks.json format
 "version": "2.0.0",
 "tasks": [
 {
  "label": "echo",
  "type": "shell",
  "command": "tsc",
  "args": ["-w", "-p","."],
  "problemMatcher": [
   "$tsc-watch"
   ],
  "isBackground": true
  }
 ]
}

在app.ts所在的目录,建立另外一个文件tsconfig.json。把下面的代码拷贝进去:

{
  "compilerOptions": {
    "sourceMap": true
  }
}

这样,Task Runner就能够把TypeScript编译成JavaScript,而且可监听到文件的变化,实时编译。

再次点击Ternimal标签,选择Run Build Task,再选择tsc: watch - tsconfig.json,能够看到终端出现的信息:

[21:41:31] Starting compilation in watch mode…

你可使用VS Code的调试功能编译TypeScript文件。  
139239-20190711145257756-261558000.png

设置好了开发环境,你就能够着手处理TypeScript泛型概念相关的问题了。

找到问题

TypeScript中不建议使用any类型,缘由有几点,你能够在本文看到。其中一个缘由,就是调试时缺少完整的信息。而选择VS Code做为开发工具的一个很好的理由,就是它带来的基于这些信息的智能感知。

若是你有一个类,存储着一个集合。有方法向该集合里添加东西,也有方法经过索引获取集合里的东西。像这样:

class Collection {
  private _things: string[];
  constructor() {
    this._things = [];
  }
  add(something: string) {
    this._things.push(something);
  }
  get(index: number): string {
    return this._things[index];
  }
}

你能够很快辨识出,此集合被显示定义为一个string类型的集合,显然是不能在其中使用number的。若是想要处理number的话,能够建立一个接受number而不是string的集合。着是一个不错的选择,但有一个很大的缺点——代码重复。代码重复,最终会致使编写和调试代码的时间增多,而且下降内存的使用效率。

另外一个选择,是使用any类型代替string类型定义刚才的类,像下面这样:

class Collection {
  private _things: any[];
  constructor() {
    this._things = [];
  }
  add(something: any) {
    this._things.push(something);
  }
  get(index: number): any {
    return this._things[index];
  }
}

此时,该集合支持你给出的任何类型。若是你建立像这样的逻辑构建此集合的话:

let Stringss = new Collection();
Stringss.add("hello");
Stringss.add("world");

这添加了字符串"hello"和"world"到集合中,你能够打出像length这样的属性,返回任意一个集合元素的长度。  

console.log(Stringss.get(0).length);

字符串"hello"有五个字符,运行TypeScript代码,你能够在调试模式下看到它。  

139239-20190711145402835-1118876491.png

请注意,当你鼠标悬停在length属性上时,VS Code的智能感知没有提供任何信息,由于它不知道你选择使用的确切类型。当你像下面这样,把其中一个添加的元素修改成其余类型时,好比number,这种不能被智能感知到的状况会体现得更加明显:

let Strings = new Collection();
Strings.add(001);
Strings.add("world");
console.log(Strings.get(0).length);

你打出一个undefined的结果,仍然没有什么有用信息。若是你更进一步,决定打印string的子字符串——它会报运行时错误,但不指不出任何具体的内容,更重要的是,编译器没有给出任何类型不匹配的编译时错误。  

1
console.log(Stringss.get(0).substr(0,1));

139239-20190711145433850-174101241.png
这仅仅是使用any类型定义该集合的一种后果罢了。

理解中心思想

刚才使用any类型致使的问题,能够用TypeScript中的泛型来解决。其中心思想是类型安全。使用泛型,你能够用一种编译器能理解的,而且合乎咱们判断的方式,指定类、类型和接口的实例。正如在其余强类型语言中的状况同样,用这种方法,就能够在编译时发现你的类型错误,从而保证了类型安全。

泛型的语法像这样:

function identity<T>(arg: T): T {
  return arg;
}

你能够在以前建立的集合中使用泛型,用尖括号括起来。  

class Collection<T> {
  private _things: T[];
  constructor() {
    this._things = [];
  }
  add(something: T): void {
    this._things.push(something);
  }
  get(index: number): T {
    return this._things[index];
  }
}
let Stringss = new Collection<String>();
Stringss.add(001);
Stringss.add("world");
console.log(Stringss.get(0).substr(0, 1));

若是将带有尖括号的新逻辑复制到代码编辑器中,你会当即注意到"001"下的波浪线。这是由于,TypeScript如今能够从指定的泛型类型推断出001不是字符串。在T出现的地方,就可使用string类型,这就实现了类型安全。本质上,这个集合的输出能够是任何类型,但你指明了它应该是string类型,因此编译器推断它就是string类型。这里使用的泛型声明是在类级别,它也能够在其余级别定义,如静态方法级别和实例方法级别,你稍后会看到。

使用泛型

你能够在泛型声明中,包含多个类型参数,它们只须要用逗号分隔,像这样:

class Collection<T, K> {
  private _things: K[];
  constructor() {
    this._things = [];
  }
  add(something: K): void {
    this._things.push(something);
  }
  get(index: number): T {
    console.log(index);
  }
}

声明时,类型参数也能够在函数中显式使用,好比:

class Collection {
  private _things: any[];
  constructor() {
    this._things = [];
  }
  add<A>(something: A): void {
    this._things.push(something);
  }
  get<B>(index: number): B {
    return this._things[index];
  }
}

所以,当你要建立一个新的集合时,在方法级别声明的泛型,如今也会在方法调用级别中被指示,像这样:  

let Stringss = new Collection();
Stringss.add<string>("hello");
Stringss.add("world");

你还可注意到,在鼠标悬停时,VS Code智能感知可以推断出第二个add函数调用仍然是string类型。

泛型声明一样适用于静态方法:

static add<A>(something: A): void {
  _things.push(something);
}

虽然初始化静态方法时,可以使用泛型类型,可是,对初始化静态属性则不能。

泛型约束

如今,你已经对泛型有比较好的认识,是时候提到泛型的核心缺点及其实用的解决方案了。使用泛型,许多属性的类型都能被TypeScript推断出来,然而,在某些TypeScript不能作出准确推断的地方,它不会作任何假设。为了类型安全,你须要将这些要求或者约束定义为接口,并在泛型初始化中继承它们。

若是你有这样一个很是简单的函数:

function printName<T>(arg: T) {
  console.log(arg.length);
  return arg;
}
printName(3);

由于TypeScript没法推断出arg参数是什么类型,不能证实全部类型都具备length属性,所以不能假设它是一个字符串(具备length属性)。因此,你会在length属性下看到一条波浪线。如前所述,你须要建立一个接口,让泛型的初始化能够继承它,以便编译器再也不报警。  

interface NameArgs {
  length: number;
}

你能够在泛型声明中继承它:

function printName<T extends NameArgs>(arg: T) {
  console.log(arg.length);
  return arg;
}

这告诉TypeScript,可以使用任何具备length属性的类型。 定义它以后,函数调用语句也必须更改,由于它再也不适用于全部类型。 因此它应看起来是这样:  

printName({length: 1, value: 3});

这是一个很基础的例子。但理解了它,你就能看到在使用泛型时,设置泛型约束是多么有用。

为何是泛型

一个活跃于Stack Overflow社区的成员,Behrooz,在后续内容中很好的回答了这个问题。在TypeScript中使用泛型的主要缘由是使类型,类或接口充当参数。 它帮助咱们为不一样类型的输入重用相同的代码,由于类型自己可用做参数。

泛型的一些好处有:

定义输入和输出参数类型之间的关系。好比
 

function test<T>(input: T[]): T {
  //…
}

 

容许你确保输入和输出使用相同的类型,尽管输入是用的数组。

可以使用编译时更强大的类型检查。在上诉示例中,编译器让你知道数组方法可用于输入,任何其余方法则不行。
你能够去掉不须要的强制类型转换。好比,若是你有一个常量列表:

Array<Item> a = [];

变量数组时,你能够由智能感知访问到Item类型的全部成员。

其余资源

结论

你已经看完了泛型概念的概述,并看到了各类示例来帮助揭示它背后的思想。 起初,泛型的概念可能使人困惑,我建议,把本文再读一遍,并查阅本文所提供的额外资源,帮助本身更好地理解。泛型是一个很棒的概念,能够帮助咱们在JavaScript中,更好地控制输入和输出。请快乐地编码吧!


本文是由葡萄城技术开发团队发布,转载请注明出处:葡萄城官网

了解可嵌入您系统的在线 Excel,请前往SpreadJS纯前端表格控件

相关文章
相关标签/搜索