TypeScript 从听说到入门(上篇)

3,093 阅读9分钟

我为什么会这样念念又不忘 / 你用什么牌的箭刺穿我心脏

我也久经沙场 / 戎马生涯 / 依然 / 被一箭刺伤

——李荣浩《念念又不忘》

接下来我会分上、下两篇文章介绍 TypeScript。

我也是 TypeScript 初学者,这两篇文章是我的学习笔记,来源于一个系列的免费视频,视频不错,如果你觉得看视频学习更快,可以点击这里看到

TypeScript 与 JavaScript 有何区别呢?简单点说,就是前者引入了类型约束,能实现代码的静态检查,而且能提供更加完善的代码提示功能;除此之外,还引入了接口、抽象类、枚举、访问控制修饰符、泛型等语法,让我们写出的代码更具健壮性和可扩展性。

上、下两篇的文章内容安排如下:

  • 《上篇》
    1. 基本类型
    2. 对象类型
    3. 扩展类型
  • 《下篇》
    1. 面向对象编程
    2. 泛型

《上篇》定义为基础篇,《下篇》定义为深入篇。

我们先从基础篇开始学习。

开发环境

TypeScript 脚本以 .ts 后缀结尾。我使用 VSCode 学习 TypeScript,写好 TypeScript 代码后, 需要将其编译为 JavaScript 代码才能运行。在此之前,我们先要安装 Node.js 环境。

然后安装 TypeScript 编译环境

$ npm install -g typescript
$ tsc --version

编译文件的指令如下:

// 执行完下列语句后,会在同级目录下看到一个 `HelloWorld.js` 文件,
// 就是编译完成后的文件啦
$ tsc HelloWorld.ts

codepen.io 中也支持 TypeScript 的书写。新建一个新的 Pen 后,将 JavaScript 一栏的预处理器设置成 TypeScript 即可,你也可以实时查看到编译之后的代码,不过代码提示效果不是很强。

基本类型

我们先从最简单的基本数据类型(Primitive)说起。

ECMAScript 提供了六种基本数据类型:布尔、数值、字符串、Null、Undefined 和 Symbol。

而 TypeScript 针对上述的每一种类型,都提供了对应的类型字面值:boolean、number、string、null、undefiend 和 symbol(ES6 中引入,本系列两篇不对 symbol 做介绍)。

在 TypeScript 中,使用 : type 语法为变量指定类型:

// 声明一个布尔值类型变量 `isMale`,初始值为 `true`
let isMale: boolean = true;
// 声明一个字符串类型变量 `myName`,初始值为 `'Alex'`
let myName: string = 'Alex';
// 声明一个数值类型变量 `myAge`,初始值为 `20`
let myAge: number = 20;
// 声明一个 Null 类型变量 `myGirlFriend`,值为 `null`
let myGirlFriend: null = null;
// 声明一个 Undefined 类型变量 `myHouse`,值为 `undefined`
let myHouse: undefined = undefined;

注意:TypeScript 提供的类型字面值都是小写形式,注意与首字母大写的形式区分,后者是 JavaScript 原生提供的构造器函数。

有时,一个变量的类型并不局限于一种。比如,一个变量的值可以是字符串,也可以是数值。这时就要用到“联合类型”了。

联合类型使用竖线 | 分隔,表示某个变量可以给予其中任意一种类型值。

下例中,声明了一个变量 foo,它的值可以是一个字符串,也可以是一个数值。

// 此处声明了一个变量 `foo`,可以是字符串,也可以是数值
let foo: string | number = 'bar'; // 初始值给了字符串 `'bar'`
foo = 123; // 接下来将 `foo` 重新赋值为 `123`

对象类型

除了基本类型,工作中最常处理就是对象了。那么如何在 TypeScript 中指定对象类型呢?

定义对象

在 TypeScript 中,使用接口,也就是关键字 interface 来描述对象的形状,也就是对象的类型。“接口”在传统的面向对象编程的语言里,比如 Java,表示“行为的抽象”,而在 TypeScript 对此稍有不同,接口不仅可以表示行为的抽象,还可以用来定义对象类型。

接下来,我们定义一个类型 Person(按照约定,首字母大写):

// 用接口声明一个类型 `Person`
interface Person {
    name: string;
    age: number;
}

// 将变量 `alex` 声明为 `Person` 类型
let alex: Person = {
    name: 'Alex',
    age: 20
};

注意:定义接口时,属性之间可以用分号 ;、也可以用逗号 , 分隔,甚至什么都不加也可以。

我们定义了一个类型 Person,并将变量 alex 的类型声明为 Person。那么,在给 alex 赋值时,必须严格符合类型定义:赋值对象必须由一个字符串属性 name和一个数值属性 age 组成,缺少或多出的属性,都会提示错误。

// 会提示出错(缺少一个属性)
let alex: Person = {
    name: 'Alex';
};

// 会提示错误(多了一个属性)
let alex: Person = {
    name: 'Alex',
    age: 20,
    gender: 'male'
};

定义类型时,如果想要表示某个属性是可选的,则使用 ?: type 语法声明。

// 类型 `Person` 的 `name` 属性是可选的 
interface Person {
    name: string;
    age?: number;
}

// 因为 `age` 是可选属性,所以赋值时不给也行
let alex: Person = {
    name: 'Alex'
};

除了定义可选属性,还可以定义“任意属性”。

所谓的任意属性,就是我们不确定将来会添加的属性名称是什么,但是会提前定义允许添加的属性,在不确定未来这个属性名的情况下,限制这个属性的类型。

// Person 中定义了一个任意属性,属性类型是 `any`
interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

// 我们给变量 `alex` 添加了一个任意属性 `gender`
let alex: Person = {
    name: 'Alex',
    gender: 'male
};

我们使用 [propName: string]: any 的形式,定义了一个 any 类型的任意属性。

注意,任意属性的类型,必须是上面的已知属性 nameage 类型的超集,否则会提示出错。 比如,上面我们可以将上面任意属性的类型 any 修改为 string | number 也是可以的。

说完对象,再来介绍数组。

数组类型

在数组上指定类型,本质上是限制数组成员的类型。在 TypeScript 中,使用 type[] 语法指定数组成员的类型。

下面定义了一个数组,限制其成员只能是字符串。

// 此处定义了一个数组 `myFriends`,其成员限定为只能是字符串
let myFriends: string[] = ['Alex', 'Bob'];

如果数组成员允许包含多个类型值,则使用 (type1 | type2 | ...)[] 的语法声明。

// 此处定义了一个数组 `foo`,其成员可以是字符串,也可以是数值
let foo: (string | number)[] = ['Alex', 'Bob', 123];

如果数组的成员是对象,则有如下两种声明方式:

// 方式 1:通过预定义好的类型,声明 `friends` 成员类型
interface Person {
    name: string;
}
let friends: Person[] = [ { name: 'Alex' }, { name: 'Bob' } ];

// 方式 2:直接通过字面量类型的形式,声明 `friends` 成员类型
let friends: {
    name: string
}[] = [ { name: 'Alex' }, { name: 'Bob' } ];

接下来,进入到扩展类型的学习。

扩展类型

Typescript 除了支持 JavaScript 类型之外,还提供了一些扩展类型。

首先,我们来介绍下字面量类型。

字面量类型

当我们像下面这样赋值时:

let seven: number = 7;
// ❌ 这样赋值的话会有错误,提示`Type '"Seven"' is not assignable to type 'number'`
seven = 'Seven';

注意,这里的 'Seven' 被当成了一个类型,说 'Seven' 类型不能赋值给数值类型变量 seven

其实这里的 'Seven' 是一个字符串字面量类型。

Typescript 中的字面量类型包括:字符串字面量、数值字面量和布尔值字面量。

除此之外,我们还可以使用 type 关键字定义一个新的类型:

// 此处我们定义了一个新类型 `FavoriteNumber`,这个新类型仅由三个值的集合组成
type FavoriteNumber = 'One' | 'Two' | 'Seven';
// 接下来,将变量 `seven` 声明为类型 `FavoriteNumber`,并赋值为 `'Seven'`
let seven: FavoriteNumber = 'Seven';

以上定义了一个类型 FavoriteNumber,它由三个值的集合组成(一个类型通常至少包含两个或以上的值)。变量 seven 被声明为该类型,赋值为 'Seven',这是一个有效值。如果我们给 seven 赋了一个不在 FavoriteNumber 类型之内的值,就会报错,比如:

// ❌ 此处会报错:`Type '123' is not assignable to type 'FavoriteNumber'.`
let seven: FavoriteNumber = 123;

枚举

在介绍枚举类型之前,我们先来看下面的代码:

// 此处定义了两个变量 `errorColor` 和 `infoColor`
let dangerColor = 'red';
let infoColor = 'blue';

// 添加一个判断传入颜色是否是危险色的函数
function isItDangerColor(color) {
    return color === 'red';
}

// 接下来,调用函数 `isItRed`
isItDangerColor(dangerColor); // true
isItDangerColor(infoColor); // false

上面这一小段的代码逻辑很简单,但有个小小的问题——如果表示危险的颜色由 'red' 变为 'pink' 了,那么我们就需要修改两个地方的代码。

针对这个问题,我们稍微修改下代码,引入一个表示颜色集合的变量 Colors 来解决:

// 我们使用 `Colors` 这个变量来存储逻辑中使用到的颜色集合
const Colors = {
    Danger: 'red',
    Info: 'blue'
};

// 在余下的业务逻辑中,我们使用颜色变量代替之前的颜色字面值
let dangerColor = Colors.Danger;
let infoColor = Colors.Info;

function isItDangerColor(color) {
    return color === Colors.Danger;
}

这样带来的便利是,如果 Colors.Danger 所代表的颜色值变了,只要在 Colors 中修改一下就可以了。

进一步思考,可以知道,这里的 Colors.DangerColors.Info的值具体是什么并不重要,只要能保证它们彼此不相等就行。比如,我们写成下面这样:

// 这样定义 `Colors` 依旧不会影响逻辑
const Colors = {
    Danger: 0,
    Info: 1
};

这种定义变量的方式,用 TypeScript 中的枚举来改写就是下面这样的:

// 枚举变量使用 `enum` 关键字定义
// 此处定义了一个枚举变量 `Colors`
enum Colors {
    Danger,
    Info
}

上面一段代码经过编译后,得到的 JavaScript 源码如下:

var Colors;
(function (Colors) {
  Colors[Colors["Danger"] = 0] = "Danger";
  Colors[Colors["Info"] = 1] = "Info";
})(Colors || (Colors = {}));

由此可知,

enum Colors {
    Danger,
    Info
}

// 等价于

var Colors = {
    0: 'Danger',
    1: 'Info',
    'Danger': 0,
    'Info': 1
};

我们修改下初始的例子,使用枚举来组织逻辑:

enum Colors {
    Danger, // 对应的值是 0
    Info, // 对应的值是 1
    Success // 对应的值是 2
}

// 我们将函数 `isItDanger` 的参数 `color` 类型约束为 `Colors`
// 说明此函数只接收 `Colors` 中列举的值
function isItDanger(color: Colors): boolean {
    return color === Colors.Danger;
}

// 接下来使用 `Colors.Info` 调用 `isItDanger` 函数
isItDanger(Colors.Info); // false

除了使用默认的索引值,我们还可以为枚举变量中的每一项指定值:

enum Colors {
    Red, // 对应的值是 0
    Blue = 3, // 将 Blue 值指定为 3
    Green // 接上面的 3,此处的值是 4
}

enum Colors {
    Red = 'red', // 对应的值是 'red'
    Blue = 'blue', // 对应的值是 'blue'
    Green = 'green' // 对应的值是 'green'
}

上篇完。