二、TypeScript 的好处

TypeScript 使您能够编写 JavaScript 代码。它包括静态类型和其他在面向对象语言中非常常见的特性。此外,使用 TypeScript,您可以使用 ECMAScript 6 的所有功能,因为编译器会将它们转换为当前浏览器可读的代码。

TypeScript 的一个特性是用户可以创建类型化变量,就像在 Java 或 C#中一样(例如,const VARIABLE_NAME: Type = Value),不仅如此,TypeScript 还帮助我们编写干净、组织良好的代码。这就是 Angular 团队在框架的当前版本中采用 TypeScript 的原因之一。

在我们开始之前,让我们看看官方的 TypeScript 文档是怎么说的:

"TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Any browser. Any host."

在本章中,我们将在我们的环境中全局安装 TypeScript,以便了解当 TypeScript 文件转换为 JavaScript 时会发生什么。不用担心;Angular 应用已经为我们提供了一个内置在 Angular CLI 中的 TypeScript 编译器。

在本章中,我们将讨论以下几点:

  • 安装类型脚本
  • 使用 TypeScript 的好处
  • 如何将 TypeScript 文件转换成 JavaScript 文件
  • 用静态类型编写 JavaScript 代码
  • 理解 TypeScript 中的接口、类和泛型

安装类型脚本

安装和开始使用 TypeScript 非常简单。有必要在您的机器上安装 Node.js 和 Node 包管理器(NPM)。

如果您还没有,请前往https://nodejs.org/en/download/并按照您的平台的逐步安装说明进行操作。

让我们安装 TypeScript,如下所示:

  1. 打开您的终端,键入以下命令来安装 TypeScript 编译器:
npm install -g typescript

请注意,-g标志意味着在您的机器上全局安装编译器。

  1. 让我们检查可用的 TypeScript 命令。在终端中键入以下命令:
tsc --help

前面的命令将提供大量关于 TypeScript 编译器的信息;我们将看到一个简单的例子,说明如何将 TypeScript 文件转换为 JavaScript 文件。

示例:

tsc hello.ts tsc --outFile file.js file.ts

前面几行的描述如下:

  • tsc命令编译hello.ts文件。
  • 告诉编译器创建一个输出文件,命名为hello.js

创建类型脚本项目

有些文本编辑器,比如 VS Code,给了我们把 TS 文件作为独立单元处理的能力,叫做文件范围。虽然这对于独立文件非常有用(如以下示例),但建议您始终创建一个 TypeScript 项目。然后,您可以模块化您的代码,并在将来使用文件之间的依赖注入。

TypeScript 项目是用一个名为tsconfig.json的文件创建的,该文件位于一个目录的根目录下。您需要向编译器指出哪些文件是项目的一部分、编译选项以及许多其他设置。

一个基本的tsconfig.json文件包含以下代码:

{ "compilerOptions":
  { "target": "es5",
   "module": "commonjs"
  }
}

虽然前面的代码非常简单直观,但我们只是在说明我们在项目中会使用哪个编译器,也是什么样的模块。如果代码片段表明我们使用的是 ECMAScript 5,那么所有 TypeScript 代码都将使用 ES5 语法转换为 JavaScript。

现在,让我们看看如何在tsc编译器的帮助下自动创建该文件:

  1. 创建一个名为chapter-02的文件夹。
  2. chapter-02文件夹中打开你的终端。
  3. 键入以下命令:
tsc --init

我们将看到以下内容,由tsc编译器生成:

{
"compilerOptions": {
/* Basic Options */
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"target": "es5",
/* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"module": "commonjs",
...
/* Strict Type-Checking Options */
/* Enable all strict type-checking options. */
"strict": true,
...
/* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true
/* Source Map Options */
...
/* Experimental Options */
...
}
}

请注意,我们省略了一些部分。您应该看到所有可用的选项;不过,大部分都是评论。这个时候不要担心;稍后,我们将更详细地了解一些选项。

现在,让我们创建一个 TypeScript 文件,并检查一切是否顺利。

  1. chapter-02文件夹中打开 VS Code,新建一个文件,叫做sample-01.ts

  2. sample-01.ts中添加以下代码:

console.log('First Sample With TypeScript');
  1. 回到您的终端,键入以下命令:
tsc sample-01.ts

In VS Code, you can use the integrated Terminal; on the top menu bar, click on View | Integrate Terminal [ˆ`].

请注意,另一个文件以相同的名称出现,但扩展名为.js

如果你比较两个文件,它们是完全一样的,因为我们的例子很简单,我们使用的是一个简单的console.log()函数。

由于 TypeScript 是一组超级 JavaScript,所有的 JS 特性在这里也是可用的。

TypeScript 的好处

以下是使用 TypeScript 的一些好处:

  • TypeScript 健壮、安全且易于调试。
  • TypeScript 代码在转换为 JavaScript 之前会被编译,因此我们可以在运行代码之前捕捉各种错误。
  • 支持 TypeScript 的 ide 具有改进代码完成和检查静态类型的能力。
  • TypeScript 支持 OOP(面向对象编程),包括模块、名称空间、类等。

TypeScript 的优势主要在于它已经被 Angular 团队采用;而且,由于 Angular 是用 JavaScript 开发现代 web 应用的最重要的前端框架之一,这激励了许多正在从 AngularJS 的 1.x 版本迁移到 2/4/5/6 版本的开发人员去学习它。

原因很简单,大多数 Angular 教程和示例都是用 TypeScript 编写的。

  1. 打开sample-01.ts,在console.log()功能后添加以下代码:
class MyClass {
  public static sum(x:number, y: number) {
  console.log('Number is: ', x + y);
  return x + y;
 }
}
MyClass.sum(3, 5);
  1. 回到您的终端,输入以下代码:
tsc sample-01.ts
  1. 现在,当你打开sample-01.js文件时,你会看到如下截图所示的结果:

Comparing TypeScript with generated JavaScript

注意,总和类参数(x:number, y:number)被赋予类型号。这是 TypeScript 的优势之一;然而,当我们在函数调用MyClass.sum(3, 5)中根据键入和使用数字来行动时,我们看不到它的力量。

我们来做个小改动,看看区别。

  1. MyClass.sum()函数调用改为MyClass.sum('a', 5)

  2. 回到您的终端,键入以下命令:

tsc sample-01.ts

请注意,我们收到一个 TypeScript 错误:

error TS2345: Argument of type '"a"' is not assignable to parameter of type 'number'.

如果您使用的是 VS Code,那么在执行编译文件的命令之前,您会看到下面截图中的消息:

Compiling error message

如前所述,VS Code 是 TypeScript 语言的强大编辑器;除了有一个集成的终端,我们还能够清楚地看到编译错误。

我们可以对 TS 文件进行一些修改,而不是每次都键入相同的命令。我们可以使用--watch标志,编译器会自动运行我们对文件所做的每一个更改。

  1. 在终端中,键入以下命令:
tsc sample-01.ts --watch
  1. 现在,让我们解决它;返回 VS 代码,用以下代码替换MyClass.sum()功能:
MyClass.sum(5, 5);

要停止 TS 编译器,只需按 Ctrl + C

用静态类型编写 JavaScript 代码

使用 TypeScript 时,除了所有 JavaScript 类型之外,您首先会注意到它的静态类型,如下表所示:

| 图元 | 物体 | | 线 | 功能 | | 数字 | 排列 | | 空 | 原型 | | 不明确的 | | | 布尔代数学体系的 | | | 标志 | |

这意味着您可以声明变量的类型;将类型赋给变量非常简单。让我们看一些例子,只使用 JavaScript 类型:

function Myband () {
  let band: string;
  let active: boolean;
  let numberOfAlbuns: number;
}

使用 TypeScript,我们还有一些类型,我们将在下面的部分中看到。

创建元组

元组就像一个有组织的类型化数组。让我们创建一个来看看它是如何工作的:

  1. chapter-02文件夹内,创建一个名为tuple.ts的文件,并添加以下代码:
const organizedArray: [number, string, boolean] = [0, 'text',
      false];
let myArray: [number, string, boolean];
myArray = ['text', 0, false]
console.log(myArray);

前面的代码看起来很适合 JavaScript,但是在 TypeScript 中,我们必须尊重变量类型;在这里,我们试图传递一个字符串,我们必须传递一个数字。

  1. 在终端中,键入以下命令:
tsc tuple.ts

您将看到以下错误消息:

tuple.ts(4,1): error TS2322: Type '[string, number, false]' is not assignable to type '[number, string, boolean]'.
 Type 'string' is not assignable to type 'number'.

In VS Code, you will see the error message before you compile your file. This is a very helpful feature.

当我们用正确的顺序(myArray = [0, 'text', false])修复时,错误信息消失。

还可以创建一个元组类型,并使用它来分配一个变量,如我们在下一个示例中所见。

  1. 回到您的终端,将以下代码添加到tuple.ts文件中:
// using tuple as Type
type Tuple = [number, string, boolean];
let myTuple: Tuple;
myTuple = [0, 'text', false];
console.log(myTuple);

在这一点上,你可能想知道为什么前面的例子有一个console.log输出。

借助我们之前安装的 Node.js,我们可以运行示例并查看console.log()函数的输出。

  1. 在终端中,键入以下命令:
node tuple.js

Note that you will need to run the JavaScript version, as in the previous example. If you try to run the TypeScript file directly, you will probably receive an error message.

使用空类型

在 TypeScript 中,必须定义函数的返回类型。当我们有一个没有返回的函数时,我们使用一个名为void的类型。

让我们看看它是如何工作的:

chapter-02文件夹内新建一个名为void.ts的文件,并添加以下代码:

function myVoidExample(firstName: string, lastName: string): string {
    return firstName + lastName;
}
console.log(myVoidExample('Jhonny ', 'Cash'));

在前面的代码中,一切正常,因为我们的函数返回值。如果我们删除返回功能,我们将看到以下错误消息:

void.ts(1,62): error TS2355: A function whose declared type is neither 'void' nor 'any' must return a value.

在 VS 代码中,您会看到以下内容:

VS Code output error

要修复它,将类型string替换为void:

function myVoidExample(firstName: string, lastName: string): void {
const name = firstName + lastName;
}

这非常有用,因为我们的函数并不总是返回值。但是请记住,我们不能在函数中声明返回值。

选择退出类型检查-任何

any类型在我们不知道从函数中期待什么时非常有用(换句话说,当我们不知道要返回哪种类型时):

  1. chapter-02文件夹内新建一个名为any.ts的文件,并添加以下代码:
let band: any;
band = {
    name: "Motorhead",
    description: "Heavy metal band",
    rate: 10
}
console.log(band);
band = "Motorhead";
console.log(band);

注意第一个band赋值是一个对象,第二个是一个字符串。

  1. 回到你的终端,编译并运行这段代码;键入以下命令:
tsc any.ts
  1. 现在,让我们看看输出。键入以下命令:
node any.js

您将在终端中看到以下消息:

{ name: 'Motorhead', description: 'Heavy metal band', rate: 10 }
 Motorhead

在这里,我们可以将任何东西分配给我们的band变量。

使用枚举

enum允许我们用更直观的名称对值进行分组。有些人更喜欢叫列举的名单和其他一些名字。让我们看一个例子,以便更容易理解这在实践中是如何工作的:

  1. chapter-02文件夹中创建一个名为enum.js的文件,并添加以下代码:
enum bands {
    Motorhead,
    Metallica,
    Slayer
}
console.log(bands);
  1. 在终端中,键入以下命令来传输文件:
tsc enum.ts
  1. 现在,让我们执行文件。键入以下命令:
node enum.js

您将在终端中看到以下结果:

{ '0': 'Motorhead',
 '1': 'Metallica',
 '2': 'Slayer',
 Motorhead: 0,
 Metallica: 1,
 Slayer: 2 }

我们现在可以通过使用名称而不是位置来获取值。

  1. console.log()功能之后添加以下代码行:
let myFavoriteBand = bands.Slayer;
console.log(myFavoriteBand);

现在,执行步骤 2步骤 3 中的命令检查结果。您将在终端中看到以下输出:

{ '0': 'Motorhead',
 '1': 'Metallica',
 '2': 'Slayer',
 Motorhead: 0,
 Metallica: 1,
 Slayer: 2 }
 My Favorite band is:  Slayer

请注意,band对象中声明的所有值(带名)都被转换为字符串,位于索引对象内部,如您在前面的示例中所见。

使用从不类型

TypeScript 2.0 中引入了 never 类型;它意味着一个从未发生过的价值。乍一看,这似乎很奇怪,但在某些情况下可以使用。

让我们看看官方文件是怎么说的:

The never type represents the types of values that never occur. Specifically, never is the return type for functions that never return, and never is the type for variables under type guards that are never true.

假设在另一个函数中调用的消息传递函数指定了回调。

它看起来像下面的代码:

const myMessage = (text: string): never => {
    throw new Error(text);
}
const myError = () => Error('Some text here');

另一个例子是同时检查字符串和数字的值,如下所示:

function neverHappen(someVariable: any) {
    if (typeof someVariable === "string" && typeof someVariable ===
     "number") {
    console.log(someVariable);
    }
}
neverHappen('text');

类型:未定义和空

在 TypeScript 中,undefinednull本身就是类型;这意味着 undefined 是一个类型(undefined),null 是一个类型(null)。困惑?未定义和 null 不能是类型变量;它们只能被赋值给变量。

它们也是不同的:空变量意味着变量被设置为空,而未定义的变量没有赋值。

let A = null;
    console.log(A) // null
    console.log(B) // undefined

理解 TypeScript 中的接口、类和泛型

面向对象编程 ( OOP )是一个非常古老的编程概念,用于 Java、C#等语言。

使用 TypeScript 的优势之一是能够将这些概念引入到您的 JavaScript web 应用中。除了能够使用类、接口等,我们还可以轻松地扩展导入类和导入模块,我们将在接下来的示例中看到这一点。

我们知道在纯 JavaScript 中使用类已经是一种选择,使用了 ECMAScript 5。虽然相似,但也有一些不同;我们不会在本章中讨论它们,这样我们就不会混淆我们的读者。我们将只关注 TypeScript 中采用的实现。

创建类

理解 TypeScript 中的类的最好方法是创建一个。一个简单的类看起来像下面的代码:

class Band {
    public name: string;
    constructor(text: string) {
    this.name = text;
    }
}

让我们创建第一个类:

  1. 打开文本编辑器,新建一个名为my-first-class.ts的文件,并添加以下代码:
class MyBand {
    // Properties without prefix are public
    // Available is; Private, Protected
    albums: Array<string>;
    members: number;
    constructor(albums_list: Array<string>, total_members: number) {
        this.albums = albums_list;
        this.members = total_members;
    }
    // Methods
    listAlbums(): void {
        console.log("My favorite albums: ");
        for(var i = 0; i < this.albums.length; i++) {
            console.log(this.albums[i]);
        }
    }
}
// My Favorite band and his best albums
let myFavoriteAlbums = new MyBand(["Ace of Spades", "Rock and Roll", "March or Die"], 3);
// Call the listAlbums method.
console.log(myFavoriteAlbums.listAlbums());

为了便于理解,我们在前面的代码中添加了一些注释。

一个类可以有任意多的方法。在上节课的情况下,我们只给出了一个方法,列出我们最喜欢的乐队专辑。您可以在您的终端上测试这段代码,在新的MyBand()构造函数中传递您想要的任何信息。

这很简单,如果你接触过 Java、C#甚至 PHP,你就已经看到了这个类结构。

在这里,我们可以将继承(OOP)原则应用于我们的类。让我们看看怎么做:

  1. 打开band-class.ts文件,在console.log()功能后添加以下代码:
/////////// using inheritance with TypeScript ////////////
class MySinger extends MyBand {
    // All Properties from MyBand Class are available inherited here
    // So we define a new constructor.
    constructor(albums_list: Array<string>, total_members: number) {
        // Call the parent's constructor using super keyword.
        super(albums_list, total_members);
    }
    listAlbums(): void{
        console.log("Singer best albums:");
        for(var i = 0; i < this.albums.length; i++) {
            console.log(this.albums[i]);
        }
    }
}
// Create a new instance of the YourBand class.
let singerFavoriteAlbum = new MySinger(["At Falson Prision", "Among out the Stars", "Heroes"], 1);
console.log(singerFavoriteAlbum.listAlbums());

In Angular, classes are very useful for defining components, as we will see in the Chapter 3, Understand the core concepts of Angular 6.

声明接口

当我们使用 TypeScript 时,接口是我们的盟友,因为它们不存在于纯 JavaScript 中。它们是分组和键入变量的有效方式,确保它们始终在一起,维护一致的代码。

让我们看一个声明和使用接口的实用方法:

  1. 在文本编辑器中,创建一个名为band-interface.ts的新文件,并添加以下代码:
interface Band {
    name: string,
    total_members: number
}

要使用它,请将接口分配给函数类型,如下例所示。

  1. band-interface.ts文件中,在界面代码后添加以下代码:
interface Band {
    name: string,
    total_members: number
}
function unknowBand(band: Band): void {
    console.log("This band: " + band.name + ", has: " +                 band.total_members + " members");
}

注意,这里我们使用Band界面输入我们的function参数。因此,当我们尝试使用它时,我们需要在新对象中保持相同的结构,如下例所示:

// create a band object with the same properties from Band interface:
let newband = {
    name: "Black Sabbath",
    total_members: 4
}
console.log(unknowBand(newband));

Note that you can execute all of the sample files by typing the command tsc band-interface.ts and the band-interface.js node in your Terminal.

因此,如果您遵循前面的提示,您将在终端窗口中看到相同的结果:

This band: Black Sabbath, has: 4 members

如您所见,TypeScript 中的接口令人难以置信;我们可以用它们做很多事情。在本书的整个过程中,我们将会看到更多在真实的 web 应用中使用接口的例子。

创建泛型函数

泛型是创建灵活类和函数的非常有用的方式。它们与 C#中使用的非常相似。在多个地方使用非常有用。

我们可以通过在函数名后添加尖括号并封闭数据类型来创建泛型函数,如下例所示:

function genericFunction<T>( arg: T ): T [] {
    let myGenericArray: T[] = [];
    myGenericArray.push(arg);
    return myGenericArray;
}

请注意,尖括号(<t>)内的t表示genericFunction()属于通用类型。

让我们在实践中看到这一点:

  1. 在代码编辑器中,创建一个名为generics.ts的新文件,并添加以下代码:
function genericFunction<T>( arg: T ): T [] {
    let myGenericArray: T[] = [];
    myGenericArray.push(arg);
    return myGenericArray;
}
let stringFromGenericFunction = genericFunction<string>("Some string goes here");
console.log(stringFromGenericFunction[0]);
let numberFromGenericFunction = genericFunction(190);
console.log(numberFromGenericFunction[0]);

让我们看看我们的泛型函数会发生什么。

  1. 回到您的终端,键入以下命令:
tsc generics.ts
  1. 现在,让我们使用以下命令执行该文件:
node generics.js

我们将看到以下结果:

Some string goes here
 190

请注意,编译器能够识别我们作为function参数传递的数据类型。在第一种情况下,我们显式地将参数作为字符串传递,在第二种情况下,我们不传递任何内容。

虽然编译器能够识别我们正在使用的参数类型,但是始终确定我们要传递什么类型的数据是很重要的。例如:

let numberFromGenericFunction = genericFunction<number>(190);
console.log(numberFromGenericFunction[0]);

使用模块

使用 TypeScript 开发大规模应用时,模块非常重要。它们允许我们导入和导出代码、类、接口、变量和函数。这些功能在使用 Angular 的应用中非常常见。

然而,它们只能通过使用一个库来完成,这个库可以是浏览器的 Require.js,或者是节点的 Common.js

在接下来的几节中,我们将说明如何在实践中使用这些功能。

使用类导出功能

正如我们前面提到的,任何申报都可以导出;为此,我们只需要添加export关键字。在下面的例子中,我们将导出band类。

在文本编辑器中,创建一个名为export.ts的文件,并添加以下代码:

export class MyBand {
    // Properties without prefix are public
    // Available is; Private, Protected
    albums: Array<string>;
    members: number;
    constructor(albums_list: Array<string>, total_members: number) {
        this.albums = albums_list;
        this.members = total_members;
    }
    // Methods
    listAlbums(): void {
        console.log("My favorite albums: ");
        for(var i = 0; i < this.albums.length; i++) {
            console.log(this.albums[i]);
        }
    }
}

我们现在可以将我们的Myband类导入到另一个文件中。

导入和使用外部类

导入可以通过关键字import完成,并且可以根据您使用的库以不同的方式声明。使用 Require.js 的一个例子如下:

  • 回到文本编辑器,创建一个名为import.ts的文件,并添加以下代码:
import MyBand = require('./export');
console.log(Myband());

使用 Common.js 的一个例子如下:

import { MyBand } from './export';
console.log(new Myband(['ZZ Top', 'Motorhead'], 3));
  • 第二种方法已经被 Angular 团队采用,因为 Angular 使用了 Webpack,一个构建现代网络应用的模块捆绑器。

摘要

在本章中,您看到了 TypeScript 的基本原理。我们只是触及了表面,但是我们为您提供了使用 TypeScript 处理 Angular 应用的坚实基础。

在本书的整个过程中,我们将随着 web 应用的创建而加深您的理解。