我的github

从 JavaScript 程序员的角度总结思考,循序渐进的理解 TypeScript。

http://ts.xcatliu.com/

什么是TypeScript?

TypeScript是一种添加了类型系统的 JavaScript,适用于任何规模的项目。

我们都知道JavaScript是一种弱类型的语言。而TypeScript增强了它的类型。

由于JavaScript 是一门非常灵活的编程语言,这将导致:

  • 它没有类型约束,一个变量可能初始化时是字符串,过一会儿又被赋值为数字。
  • 由于隐式类型转换的存在,有的变量的类型很难在运行前就确定。
  • 基于原型的面向对象编程,使得原型上的属性或方法可以在运行时被修改。
  • 函数是 JavaScript 中的一等公民[2],可以赋值给变量,也可以当作参数或返回值。

这种灵活性就像一把双刃剑,一方面使得 JavaScript 蓬勃发展,无所不能,从 2013 年开始就一直蝉联最普遍使用的编程语言排行榜冠军;另一方面也使得它的代码质量参差不齐,维护成本高,运行时错误多。

而 TypeScript 的类型系统,在很大程度上弥补了 JavaScript 的缺点。它是静态语言。

而JavaScript是动态语言,如下面的代码直到运行时才会报错:

let foo = 1;
foo.split(' ');

同样是这段代码,在TypeScript下编译时就会报错了。

let foo = 1;
foo.split(' ');

你可能会奇怪,这段 TypeScript 代码看上去和 JavaScript 没有什么区别呀。没错!大部分 JavaScript 代码都只需要经过少量的修改(或者完全不用修改)就变成 TypeScript 代码,这得益于 TypeScript 强大的[类型推论][],即使不去手动声明变量 foo 的类型,也能在变量初始化时自动推论出它是一个 number 类型。

完整的 TypeScript 代码是这样的:

let foo: number = 1;
foo.split(' ');

TypeScript 是弱类型:

类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。

以下这段代码不管是在 JavaScript 中还是在 TypeScript 中都是可以正常运行的,运行时数字 1 会被隐式类型转换为字符串 '1',加号 + 被识别为字符串拼接,所以打印出结果是字符串 '11'

console.log(1 + '1');

作为对比,Python是强类型的。需要进行强制类型转换。

这样的类型系统体现了 TypeScript 的核心设计理念:在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的 bug。

TypeScript非常适合于大型项目。在中小型项目中推行 TypeScript 的最大障碍就是认为使用 TypeScript 需要写额外的代码,降低开发效率。但事实上,由于有[类型推论][],大部分类型都不需要手动声明了。TypeScript 还可以和 JavaScript 共存。这意味着如果你有一个使用 JavaScript 开发的旧项目,又想使用 TypeScript 的特性,那么你不需要急着把整个项目都迁移到 TypeScript,你可以使用 TypeScript 编写新文件,然后在后续更迭中逐步迁移旧文件。如果一些 JavaScript 文件的迁移成本太高,TypeScript 也提供了一个方案,可以让你在不修改 JavaScript 文件的前提下,编写一个[类型声明文件][],实现旧项目的渐进式迁移。

事实上,就算你从来没学习过 TypeScript,你也可能已经在不知不觉中使用到了 TypeScript——在 VSCode 编辑器中编写 JavaScript 时,代码补全和接口提示等功能就是通过 TypeScript Language Service 实现的。

一些第三方库原生支持了 TypeScript,在使用时就能获得代码补全了,比如 Vue 3.0

有一些第三方库原生不支持 TypeScript,但是可以通过安装社区维护的类型声明库(比如通过运行 npm install --save-dev @types/react 来安装 React 的类型声明库)来获得代码补全能力——不管是在 JavaScript 项目中还是在 TypeScript 中项目中都是支持的:

  • 2016-05:@types/react 发布,TypeScript 可以开发 React 应用了。
  • 2016-05:@types/node 发布,TypeScript 可以开发 Node.js 应用了。
  • 2020-09:Vue 发布了 3.0 版本,官方支持 TypeScript。

安装TypeScript

TypeScript 的命令行工具安装方法如下:

npm install -g typescript

以上命令会在全局环境下安装 tsc 命令,安装完成之后,我们就可以在任何地方执行 tsc 命令了。

编译一个 TypeScript 文件很简单:

tsc hello.ts

我们约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx 为后缀。

Hello TypeScript

我们从一个简单的例子开始。

将以下代码复制到 hello.ts 中:

function sayHello(person: string) {
    return 'Hello, ' + person;
}

let user = 'Tom';
console.log(sayHello(user));

然后执行

tsc hello.ts

这时候会生成一个编译好的文件 hello.js

function sayHello(person) {
    return 'Hello, ' + person;
}
var user = 'Tom';
console.log(sayHello(user));

上述例子中,我们用 : 指定 person 参数类型为 string。但是编译为 js 之后,并没有什么检查的代码被插入进来。

这是因为 TypeScript 只会在编译时对类型进行静态检查,如果发现有错误,编译的时候就会报错。而在运行时,与普通的 JavaScript 文件一样,不会对类型进行检查。

如果我们需要保证运行时的参数类型,还是得手动对类型进行判断:

function sayHello(person: string) {
    if (typeof person === 'string') {
        return 'Hello, ' + person;
    } else {
        throw new Error('person is not a string');
    }
}

let user = 'Tom';
console.log(sayHello(user));

let 是 ES6 中的关键字,和 var 类似,用于定义一个局部变量,可以参阅 let 和 const 命令

下面尝试把这段代码编译一下:

function sayHello(person: string) {
    return 'Hello, ' + person;
}

let user = [0, 1, 2];
console.log(sayHello(user));

编辑器中会提示错误,编译的时候也会出错。

但是还是生成了 js 文件。

这是因为 TypeScript 编译的时候即使报错了,还是会生成编译结果,我们仍然可以使用这个编译之后的文件。

如果要在报错的时候终止 js 文件的生成,可以在 tsconfig.json 中配置 noEmitOnError 即可。关于 tsconfig.json,请参阅官方手册中文版)。

基础

本部分介绍了 TypeScript 中的常用类型和一些基本概念,旨在让大家对 TypeScript 有个初步的理解。具体内容包括:

原始数据类型

JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。 原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt。 本节主要介绍前五种原始数据类型在 TypeScript 中的应用。

布尔值

数值

字符串

空值

Null 和 Undefined

任意值

任意值(Any)用来表示允许赋值为任意类型。

什么是任意值类型?

如果是一个普通类型,在赋值过程中改变类型是不被允许的。但如果是 any 类型,则允许被赋值为任意类型。

任意值的属性和方法:

在任意值上访问任何属性都是允许的。

也允许调用任何方法。

可以认为,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。

未声明类型的变量

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型。

类型推论

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。

什么是类型推论

以下代码虽然没有指定类型,但是会在编译的时候报错:

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。

简单的例子

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

访问联合类型的属性或方法

对象的类型——接口

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。

什么是接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

简单的例子

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

上面的例子中,我们定义了一个接口 Person,接着定义了一个变量 tom,它的类型是 Person。这样,我们就约束了 tom 的形状必须和接口 Person 一致。

可选属性

有时我们希望不要完全匹配一个形状,那么可以用可选属性。

任意属性

有时候我们希望一个接口允许有任意的属性,可以使用如下方式。

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性。

数组的类型

在 TypeScript 中,数组类型有多种定义方式,比较灵活。

「类型 + 方括号」表示法

数组泛型

用接口表示数组

接口也可以用来描述数组:

interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字。

虽然接口也可以用来描述数组,但是我们一般不会这么做,因为这种方式比前两种方式复杂多了。

不过有一种情况例外,那就是它常用来表示类数组。

类数组

类数组(Array-like Object)不是数组类型,比如 arguments

function sum() {
    let args: number[] = arguments;
}

上例中,arguments 实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口:

function sum() {
    let args: {
        [index: number]: number;
        length: number;
        callee: Function;
    } = arguments;
}

在这个例子中,我们除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 length 和 callee 两个属性。

事实上常用的类数组都有自己的接口定义,如 IArgumentsNodeListHTMLCollection 等:

关于内置对象,可以参考内置对象一章。

 

posted on 2021-06-28 09:18  XiaoNiuFeiTian  阅读(16904)  评论(0编辑  收藏  举报