TypeScript 学习纪要
TypeScript
[toc]
什么是 TypeScript?
- TypeScript 是添加了类型系统的 JavaScript,适用于任何规模的项目。
- TypeScript 是一门静态类型、弱类型(允许隐式类型转换)的语言。
- TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性。
- TypeScript 可以编译为 JavaScript,然后运行在浏览器、Node.js 等任何能运行 JavaScript 的环境中。
- TypeScript 拥有很多编译选项,类型检查的严格程度由你决定。
- TypeScript 可以和 JavaScript 共存,这意味着 JavaScript 项目能够渐进式的迁移到 TypeScript。
- TypeScript 增强了编辑器(IDE)的功能,提供了代码补全、接口提示、跳转到定义、代码重构等能力。
- TypeScript 拥有活跃的社区,大多数常用的第三方库都提供了类型声明。
- TypeScript 与标准同步发展,符合最新的 ECMAScript 标准(stage 3)。
安装
TypeScript 的命令行工具安装方法如下:
npm install -g typescript
以上命令会在全局环境下安装 tsc
命令,安装完成之后,我们就可以在任何地方执行 tsc
命令了。
编译一个 TypeScript 文件很简单:
tsc hello.ts
我们约定使用 TypeScript 编写的文件以 .ts
为后缀,用 TypeScript 编写 React 时,以 .tsx
为后缀。
主流的编辑器都支持 TypeScript,这里我推荐使用 Visual Studio Code。
使用基础
-
字符串
`Hello, my name is ${myName}.`
等同于js的下述语句,其中
${expr}
用来在模板字符串中嵌入表达式。"Hello, my name is " + myName + ".
-
Any 任意值类型
允许被赋值为任意类型,在任意值上访问任何属性都是允许的,也允许调用任何方法
let something; something = 'seven'; something = 7; something.setName('Tom');
编译不会报错,但是如果逻辑未实现,运行依旧会报错
-
类型推论
TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成
any
类型而完全不被类型检查 -
联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种,联合类型使用
|
分隔每个类型let myFavoriteNumber: string | number; myFavoriteNumber = 'seven'; myFavoriteNumber = 7;
-
对象的类型
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
interface Person { name: string; age: number; } let tom: Person = { name: 'Tom', age: 25 };
有时我们希望不要完全匹配一个形状,那么可以用可选属性:
interface Person { name: string; age?: number; } let tom: Person = { name: 'Tom' };
有时候我们希望一个接口允许有任意的属性,可以使用如下方式:
interface Person { name: string; age?: number; [propName: string]: any; } let tom: Person = { name: 'Tom', gender: 'male' };
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集:
interface Person { name: string; age?: number; [propName: string]: string; }
上述这种写法会报错,任意属性的值允许是
string
,但是可选属性age
的值却是number
,number
不是string
的子属性,所以报错了。一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
interface Person { name: string; age?: number; [propName: string]: string | number; } let tom: Person = { name: 'Tom', age: 25, gender: 'male' };
只读属性,有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用
readonly
定义只读属性,该属性不可再次赋值interface Person { readonly id: number; name: string; age?: number; [propName: string]: any; } let tom: Person = { id: 89757, name: 'Tom', gender: 'male' };
-
数组
在 TypeScript 中,数组类型有多种定义方式,比较灵活。
最简单的方法是使用「类型 + 方括号」来表示数组,数组的项中不允许出现其他的类型:
let fibonacci: number[] = [1, 1, 2, 3, 5];
我们也可以使用数组泛型(Array Generic)
Array<elemType>
来表示数组:let fibonacci: Array<number> = [1, 1, 2, 3, 5];
-
函数的类型
函数声明的类型定义:
function sum(x: number, y: number): number { return x + y; }
注意,输入多余的(或者少于要求的)参数,是不被允许的
通过可选参数,可以实现输入多余或少于要求的参数
function buildName(firstName: string, lastName?: string) { if (lastName) { return firstName + ' ' + lastName; } else { return firstName; } } let tomcat = buildName('Tom', 'Cat'); let tom = buildName('Tom');
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了:
参数默认值:允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制了:
function buildName(firstName: string, lastName: string = 'Cat') { return firstName + ' ' + lastName; } let tomcat = buildName('Tom', 'Cat'); let tom = buildName('Tom');
剩余参数
ES6 中,可以使用
...rest
的方式获取函数中的剩余参数(rest 参数),注意,rest 参数只能是最后一个参数:function push(array, ...items) { items.forEach(function(item) { array.push(item); }); } let a: any[] = []; push(a, 1, 2, 3);
重载
我们可以使用重载定义多个
reverse
的函数类型:function reverse(x: number): number; function reverse(x: string): string; function reverse(x: number | string): number | string { if (typeof x === 'number') { return Number(x.toString().split('').reverse().join('')); } else if (typeof x === 'string') { return x.split('').reverse().join(''); } }
上例中,我们重复定义了多次函数
reverse
,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
-
类型断言
值 as 类型
类型断言的常见用途有以下几种:
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
- 要使得
A
能够被断言为B
,只需要A
兼容B
或B
兼容A
即可
-
声明文件 https://ts.xcatliu.com/basics/declaration-files.html#declare-var
1)通常我们会把声明语句放到一个单独的文件(
jQuery.d.ts
)中,这就是声明文件,声明文件必需以.d.ts
为后缀。一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当我们将 jQuery.d.ts 放到项目中时,其他所有 *.ts 文件就都可以获得 jQuery 的类型定义了。 /path/to/project ├── src | ├── index.ts | └── jQuery.d.ts └── tsconfig.json 假如仍然无法解析,那么可以检查下 tsconfig.json 中的 files、include 和 exclude 配置,确保其包含了 jQuery.d.ts 文件。
2)第三方声明文件
当然,jQuery 的声明文件不需要我们定义了,社区已经帮我们定义好了:jQuery in DefinitelyTyped。 我们可以直接下载下来使用,但是更推荐的是使用 @types 统一管理第三方库的声明文件。 @types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例: npm install @types/jquery --save-dev 可以在这个页面搜索你需要的声明文件
3)书写声明文件
当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。
进阶知识
-
类型别名
我们使用
type
创建类型别名,类型别名常用于联合类型type Name = string; type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); } }
-
字符串字面量类型
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
type EventNames = 'click' | 'scroll' | 'mousemove'; function handleEvent(ele: Element, event: EventNames) { // do something } handleEvent(document.getElementById('hello'), 'scroll'); // 没问题 handleEvent(document.getElementById('world'), 'dblclick'); // 报错,event 不能为 'dblclick' // index.ts(7,47): error TS2345: Argument of type '"dblclick"' is not assignable to parameter of type 'EventNames'.
上例中,我们使用
type
定了一个字符串字面量类型EventNames
,它只能取三种字符串中的一种。注意,类型别名与字符串字面量类型都是使用
type
进行定义。 -
元组
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。
元组起源于函数编程语言(如 F#),这些语言中会频繁使用元组。
let tom: [string, number] = ['Tom', 25];
当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:
let tom: [string, number]; tom = ['Tom', 25]; tom.push('male');
-
枚举
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。
枚举使用
enum
关键字来定义:enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
枚举成员会被赋值为从
0
开始递增的数字,同时也会对枚举值到枚举名进行反向映射:enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; console.log(Days["Sun"] === 0); // true console.log(Days["Mon"] === 1); // true console.log(Days["Tue"] === 2); // true console.log(Days["Sat"] === 6); // true console.log(Days[0] === "Sun"); // true console.log(Days[1] === "Mon"); // true console.log(Days[2] === "Tue"); // true console.log(Days[6] === "Sat"); // true
我们也可以给枚举项手动赋值:
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat}; console.log(Days["Sun"] === 7); // true console.log(Days["Mon"] === 1); // true console.log(Days["Tue"] === 2); // true console.log(Days["Sat"] === 6); // true
-
类
1)属性和方法
使用
class
定义类,使用constructor
定义构造函数通过
new
生成新实例的时候,会自动调用构造函数。2)类的继承
使用
extends
关键字实现继承,子类中使用super
关键字来调用父类的构造函数和方法class Cat extends Animal { constructor(name) { super(name); // 调用父类的 constructor(name) console.log(this.name); } sayHi() { return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi() } } let c = new Cat('Tom'); // Tom console.log(c.sayHi()); // Meow, My name is Tom
3)静态方法
使用
static
修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:4)TypeScript中类的用法
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected。 public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的 private 修饰的属性或方法是私有的,不能在声明它的类的外部访问 protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
-
类与接口
实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用
implements
关键字来实现。这个特性大大提高了面向对象的灵活性。interface Alarm { alert(): void; } class Door { } class SecurityDoor extends Door implements Alarm { alert() { console.log('SecurityDoor alert'); } } class Car implements Alarm { alert() { console.log('Car alert'); } }
一个类可以实现多个接口:
interface Alarm { alert(): void; } interface Light { lightOn(): void; lightOff(): void; } class Car implements Alarm, Light { alert() { console.log('Car alert'); } lightOn() { console.log('Car light on'); } lightOff() { console.log('Car light off'); } }
接口继承接口
接口与接口之间可以是继承关系:
interface Alarm { alert(): void; } interface LightableAlarm extends Alarm { lightOn(): void; lightOff(): void; }
这很好理解,
LightableAlarm
继承了Alarm
,除了拥有alert
方法之外,还拥有两个新方法lightOn
和lightOff
。接口继承类
常见的面向对象语言中,接口是不能继承类的,但是在 TypeScript 中却是可以的:
class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } interface Point3d extends Point { z: number; } let point3d: Point3d = {x: 1, y: 2, z: 3};
为什么 TypeScript 会支持接口继承类呢?
实际上,当我们在声明
class Point
时,除了会创建一个名为Point
的类之外,同时也创建了一个名为Point
的类型(实例的类型)。所以我们既可以将
Point
当做一个类来用(使用new Point
创建它的实例)也可以将
Point
当做一个类型来用(使用: Point
表示参数的类型)class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } interface PointInstanceType { x: number; y: number; } function printPoint(p: PointInstanceType) { console.log(p.x, p.y); } printPoint(new Point(1, 2));
声明
Point
类时创建的Point
类型只包含其中的实例属性和实例方法,不包含构造函数、静态属性、静态方法等 -
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
function createArray<T>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; } createArray<string>(3, 'x'); // ['x', 'x', 'x']
定义泛型的时候,可以一次定义多个类型参数:
function swap<T, U>(tuple: [T, U]): [U, T] { return [tuple[1], tuple[0]]; } swap([7, 'seven']); // ['seven', 7]
泛型约束
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; }
上例中,我们使用了
extends
约束了泛型T
必须符合接口Lengthwise
的形状,也就是必须包含length
属性。