从 JavaScript 程序员的角度总结思考,循序渐进的理解 TypeScript。
什么是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
两个属性。
事实上常用的类数组都有自己的接口定义,如 IArguments
, NodeList
, HTMLCollection
等:
关于内置对象,可以参考内置对象一章。