V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
powertoolsteam
V2EX  ›  推广

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

  •  
  •   powertoolsteam · 2019-07-11 15:22:27 +08:00 · 1410 次点击
    这是一个创建于 1964 天前的主题,其中的信息可能已经有所发展或是发生改变。

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


    你将在本文中学到什么

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

    准备工作

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

    安装 Node.js:你可以运行命令行检查 Node 是否安装好了。

    node -v
    

    安装 Node Package Manager: 通常安装 Node 时,它会顺带安装好所需版本的 NPM。 安装 TypeScript:如果你安装好了 Node Package Manager,你可以用以下命令在本机的全局环境安装 TypeScript。

    npm install -g typescript
    

    集成开发环境:本文将使用微软团队开发的 Visual Studio Code。可以在这里下载。进入其下载的目录,并按照提示进行安装。记得选择“添加打开代码”( Add open with code )选项,这样你就可以在本机从任何位置轻松打开 VS Code 了。

    本文是写给各层次的 TypeScript 开发人员的,包括但并不只是初学者。 这里给出了设置工作环境的步骤,是为了照顾那些 TypeScript 和 Visual Studio Code 的新手们。

    TypeScript 里的泛型是个啥

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

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

    在 VS Code 中配置 TypeScript

    在计算机中创建一个新文件夹,然后使用 VS Code 打开它(如果你跟着从头开始操作,那你已经安装好了)。

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

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

    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 文件。

    设置好了开发环境,你就可以着手处理 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 代码,你可以在调试模式下看到它。

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

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

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

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

    这仅仅是使用 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 中,更好地控制输入和输出。请快乐地编码吧!

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

    2 条回复    2019-07-18 17:03:27 +08:00
    LongMaoz
        1
    LongMaoz  
       2019-07-11 16:31:03 +08:00 via Android
    强类型是世界上最好的需要(🐶
    yimity
        2
    yimity  
       2019-07-18 17:03:27 +08:00
    说的比较清楚了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5546 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 06:41 · PVG 14:41 · LAX 22:41 · JFK 01:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.