TypeScript入门教程
一、简介
1.什么是TypeScript
官网
对TypeScript的定义:
Typed JavaScript at Any Scale.
添加了类型系统的JavaScript,适用于任何规模的项目。
1.1 TypeScript的特性
JavaScrip的特性:
- 没有类型约束,一个变量可能初始化是字符串,过一会儿又被赋值为数字
- 由于隐式类型转换的存在,有的变量的类型很难在运行前就确定
- 基于原型的面向对象的编程,使得原型上的属性或方法可以在运行时被修改
-
函数是JavaScript中的一等公民,可以赋值给变量,也可以当作参数或返回值
JavaScrip的灵活性就像一把双刃剑,另一方面也使得它的代码质量参差不齐,维护成本高,运行时错误多。
TypeScript 的类型系统,在很大程度上弥补了 JavaScript 的缺点。
1.1.1 类型系统
类型系统按照【类型检查的时机】来分类,可以分为动态类型和静态类型:
- 动态类型:是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误
- 静态类型:是指编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误
类型系统按照【是否允许隐士转换】来分类,可以分为强类型和弱类型。
- 强类型:Python;弱类型:JavaScript
- 强/弱是相对的,Python在处理整型和浮点型相加时,会将整型隐式转换为浮点型,但这并不影响Python是强类型的结论,因为大部分情况下Pyhon并不会进行隐式类型转换。
1.1.2 TypeScript的特性
- TypeScrip是静态类型
- TypeScript是弱类型
- 适用于任何规模
TypeScript是静态类型
-
JavaScript是一门解释型语音,没有编译阶段,所以它是动态类型
- 以下代码在运行时才会报错:
let foo = 1; foo.split(' '); // Uncaught TypeError: foo.split is not a function // 运行时会报错(foo.split 不是一个函数),造成线上 bug`
-
TypeScrip在运行前需要先编译为JavaScript,而在编译阶段就会进行类型检查,所以,
TypeScript是静态类型
- 以下代码在编译阶段就会报错:
let foo = 1; foo.split(' '); // Property 'split' does not exist on type 'number'. // 编译时会报错(数字没有 split 方法),无法通过编译
TypeScript是弱类型
-
TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性,所以它们都是弱类型
-
以下代码在JavaScrit和TypeScript中都可以正常运行,运行时数字1会被隐式类型转换为字符串
'1'
,加号
+
被识别为字符串拼接,打印结果是字符串
'11'
console.log(1 + '1') // 打印出字符串 '11'
-
以下代码在JavaScrit和TypeScript中都可以正常运行,运行时数字1会被隐式类型转换为字符串
-
Python是强类型
-
以下代码会在运行时报错
print(1 + '1') # TypeError: unsupported operand type(s) for +: 'int' and 'str'
- 如果要修复该错误,需要进行强制类型转换:
print(str(1) + '1') # 打印出字符串 '11'
-
以下代码会在运行时报错
TypeScript的类型系统体现了它的核心设计理念:在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的 bug
适用于任何规模
- 一些第三方库原生支持了 TypeScript,在使用时就能获得代码补全了,比如 Vue 3.0
- 有一些第三方库原生不支持 TypeScript,但是可以通过安装社区维护的类型声明库(比如通过运行 npm install –save-dev @types/react 来安装 React 的类型声明库
2. 安装TypeScript
命令行工具安装TypeScript:
-
npm install -g typescript
- 以上命令会在全局环境下安装 tsc 命令,安装完成之后,我们就可以在任何地方执行 tsc 命令了
编译一个TypeScript文件:
-
tsc hello.ts
- 我们约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx 为后缀
编辑器:
-
主流的编辑器都支持 TypeScript,推荐使用 Visual Studio Code
- Visual Studio Code本身也是用 TypeScript 编写的
二、基础
1. 原始数据类型
JavaScript的类型分为两种:
-
原始数据类型
-
原始数据类型包括:布尔值、数值、字符串、
null
、
undefined
以及ES6中的新类型
symbol
和ES10中的新类型
Bright
-
原始数据类型包括:布尔值、数值、字符串、
- 对象类型。
1.1 布尔值
返回的是布尔值:
-
布尔值是最基础的数据类型,在TypeScript中,使用
boolean
定义布尔值类型:let isDone: boolean = false;
-
直接调用
Boolean
也可以返回一个
boolean
类型:let createdByBoolean: boolean = Boolean(1);
返回的是一个对象(包装对象):
-
注意
,使用构造函数
Boolean
创造的对象
不是
布尔值:let createdByNewBoolean: boolean = new Boolean(1); // Type 'Boolean' is not assignable to type 'boolean'. // 'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.
-
new Boolean
返回的是一个
Boolean
对象:let createdByNewBoolean: Boolean = new Boolean(1);
在TypeScript中,
boolean
是JavaScript中的基本类型,而
Boolean
是JavaScript中的构造函数。其他基本类型(除了
null
和
undefined
)一样,不再赘述。
1.2 数值
使用
number
定义数值类型:
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;
编译结果:
var decLiteral = 6;
var hexLiteral = 0xf00d;
// ES6 中的二进制表示法
var binaryLiteral = 10;
// ES6 中的八进制表示法
var octalLiteral = 484;
var notANumber = NaN;
var infinityNumber = Infinity;
其中,
0b1010
和
0o744
是
ES6中的二进制和八进制表示法
,它们会被编译为十进制数字。
1.3 字符串
使用
string
定义字符串类型:
添加链接描述
let myName: string = 'Jerry';
let myAge: number = 15;
//模板字符串
let sentence:string = `hello,this is ${myName}.I'll be ${myAge + 1} years old next month.`;
编译结果:
var myName = 'Jerry';
var myAge = 15;
// 模板字符串
var sentence = "Hello, my name is " + myName + ".
I'll be " + (myAge + 1) + " years old next month.";
其中,
反引号
用来定义
ES6中的模板字符串
,
${expr}
用来在模板字符串中嵌入表达式。
1.4 空值
JavaScript中没有空值(Void)的概念,在TypeScript中,可以用
void
表示没有任何返回值的函数:
function alertName(): void {
alert('My name is Tom');
}
声明一个
void
类型的变量没有什么用,因为你只能将他赋值给
undefined
和
null
(只在 strictNullChecks——TypeScript中严格的空校验 未指定时):
let unusable: void = undefined;
1.5 Null和Undefined
在TypeScript中,可以使用
null
和
undefined
来定义这两个原始数据类型:
let u: undefined = undefined;
let n: null = null;
与
void
的区别:
undefined
和
null
是所有类型的子类型,可以把
undefined
和
null
赋值给其他类型的变量
也就是说,
undefined
类型的变量,可以赋值给
number
类型的变量:
// 这样不会报错
let num: number = undefined; //非严格模式
let num: number = undefined; // 严格模式下,会报错: Type 'undefined' is not assignable to type 'number'
// 这样也不会报错
let u: undefined; //非严格模式
let num: number = u; //非严格模式
/**
非严格模式下,变量的值可以为 undefined 或 null
而严格模式下,变量的值只能为 undefined
**/
而
void
类型的变量不能赋值给
number
类型的变量:
let u: void;
let num: number = u;
// Type 'void' is not assignable to type 'number'.
2. 任意值
任意值(
any
)用来表示允许赋值为任意类型。
2.1 什么是任意值类型
如果是一个普通类型,在赋值过程中改变类型是不被允许的:
let myFavoriteNumber: string = 'Jerry';
myFavoriteNumber = 8;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
但如果是
any
类型,则允许被赋值为任意类型:
let myFavoriteNumber: any = 'Jerry';
myFavoriteNumber = 8;
2.2 任意值的属性和方法
在任意值上访问
任何
属性都是允许的:
let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
也允许调用
任何
方法:
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');
可以认为,
声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值
2.3 未声明类型的变量
变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型
:
let something;
something = 'Jerry';
something = 7;
something.setName('Tom');
等价于:
let something: any;
something = 'Jerry';
something = 7;
something.setName('Tom');
3. 类型推论
如果没有明确地指定类型,那么TypeScript会依照类型推论(type Inference)的规则推断出一个类型。
3.1 什么是类型推论
TypeScript会在没有明确的指定类型的时候推测出一个类型
,这就是类型推论。
-
以下代码虽然没有指定类型,但会在编译的时候报错:
TS会自动推测出myFavoriteNumber的类型为
string
,所以给它赋值为
number
会报错let myFavoriteNumber = 'seven'; myFavoriteNumber = 7; // index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
实际上,它等价于:
let myFavoriteNumber: string = 'seven'; myFavoriteNumber = 7; // index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
-
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成any类型而完全不被类型检查。
let myFavoriteNumber; //等价于 let myFavoriteNumber : any ; myFavoriteNumber = 'seven'; myFavoriteNumber = 7;
4. 联合类型
联合类型(Union Type)表示取值可以为多种类型中的一种。
4.1 一个简单的例子
let myFavoriteNumber: string | number;
myFavoriteNumber = 'Jerry';
myFavoriteNumber = 8;
myFavoriteNumber = true;
// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
// Type 'boolean' is not assignable to type 'number'.
/**
let myFavoriteNumber: string | number 的含义是,允许 myFavoriteNumber 的类型是 string 或者 number,但是不能是其他类型
**/
联合类型使用
|
分隔每个类型
。
4.2 访问联合类型的属性或方法
4.2.1 当TS不确定一个联合类型的变量到底是哪个类型的时候,我们
只能访问该联合类型的所有类型里共有的属性或方法
:
-
如下代码,
length
不是
string
和
number
的共有属性,所以会报错function getLength(something: string | number): number { return something.length; } // index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'. // Property 'length' does not exist on type 'number'.
-
访问
string
和
number
的共有属性是没问题的function getString(something: string | number): string { return something.toString(); }
4.2.2 联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
//myFavoriteNumber 被推断成了 string,访问它的 length 属性不会报错
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
// myFavoriteNumber 被推断成了 number,访问它的 length 属性会报错
console.log(myFavoriteNumber.length); // 编译时报错
// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.
5. 对象的类型——接口
在TS中,我们
使用接口(Interfaces)来定义对象的类型
。
5.1 什么是接口
-
在面向对象语言中,
接口
(Interfaces)是一个很重要的概念,它
是对行为的抽象
,而具体如何行动由类(classes)去实现(implement)。 -
接口一般首字母大写
,有的编程语言中会建议接口的名称加上
I
前缀。
5.2 一个简单的例子
-
这是一个简单的例子
//定义一个接口Person interface Person { name: string; age: number; } /** 定义一个变量tom,它的类型是Person 约定tom的形状必须和接口Person一致 **/ let tom: Person = { name: 'Tom', age: 25 };
-
定义的变量比接口少/多一些属性是不允许的:
interface Person { name: string; age: number; } /** 变量tom比接口Person少一个age属性,这是不允许的 **/ let tom: Person = { name: 'Tom' }; /** 报错: index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'. Property 'age' is missing in type '{ name: string; }'. **/ // 变量jerry比接口多一个gender属性,这是不允许的 let jerry: Person = { name: 'Tom', age: 25, gender: 'male' }; /** 报错: index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'. Object literal may only specify known properties, and 'gender' does not exist in type 'Person'. **/
可见,
赋值的时候,变量的形状必须和接口的形状保持一致!
5.3 可选属性(?)
可选属性的含义是:该属性可以不存在
-
有时我们希望不要完全匹配一个形状,那么可以用可选属性。
interface Person { name: string; age?: number; } let tom: Person = { name: 'Tom' }; let jerry: Person = { name: 'Tom', age: 25 };
-
但是,仍然不允许添加未定义的属性:
interface Person { name: string; age?: number; } let tom: Person = { name: 'Tom', age: 25, gender: 'male' //添加了接口Person中未定义的属性gender }; /** 报错: examples/playground/index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'. Object literal may only specify known properties, and 'gender' does not exist in type 'Person'. **/
5.4 任意属性
有时候我们希望一个接口允许有任意的属性,可以使用如下方式:
interface Person {
name: string;
age?: number;
[propName: string]: any; //使用 [propName: string] 定义了任意属性取 string 类型的值
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};
-
需要注意的是,
一旦定义了任意属性,那么确定属性和可选属性的类型都必须是任意属性的类型的子集
interface Person { name: string; age?: number; [propName: string]: string; //任意属性的值允许string类型的属性 } let tom: Person = { name: 'Tom', age: 25, //age属性是number类型,number不是string的子属性,所以报错 gender: 'male' }; /** 报错: index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'. index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'. Index signatures are incompatible. Type 'string | number' is not assignable to type 'string'. Type 'number' is not assignable to type 'string'. **/
在报错信息中可以看出,此时
{ name: 'Tom', age: 25, gender: 'male' }
的类型被推断成了
{ [x: string]: string | number; name: string; age: number; gender: string; }
,这是联合类型和接口的结合。 -
一个接口只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
interface Person { name: string; age?: number; [propName: string]: string | number; //联合类型 } let tom: Person = { name: 'Tom', age: 25, gender: 'male' };
因为一旦定义了任意属性,确定属性和可选属性的类型必须是任意属性的子集,所以当接口有多种类型的属性时,可以将任意属性定义为联合类型。
- 在3.9.3中,如果同时存在任意属性、可选属性,那么任意属性的数据类型要带undefined
5.5 只读属性
有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以
用
readonly
定义只读属性
interface Person {
readonly id: number; //定义只读属性
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
id: 89757,
name: 'Tom',
gender: 'male'
};
tom.id = 9527; //给只读属性赋值,会报错
//index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
-
注意:
只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候
:
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
//第一次给对象赋值,会报错,因为没有给id属性赋值(报错1)
let tom: Person = {
name: 'Tom',
gender: 'male'
};
//给只读属性赋值,会报错(报错2)
tom.id = 89757;
/**
报错1:
index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
Property 'id' is missing in type '{ name: string; gender: string; }'.
报错2:
index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
**/
6. 数组的类型
在TS中,数组类型有多种定义方式,比较灵活。
6.1 [类型+方括号]表示法
最简单的方法是使用[类型+方括号]来表示数组:
//[类型+方括号]表示法
let fibonacci: number[] = [1, 1, 2, 3, 5];
//数组的项中不允许出现其他的类型
let fibonacci2: number[] = [1, '1', 2, 3, 5];
//数组的一些方法的参数也会根据数组在定义时约定的类型进行限制
let fibonacci3: number[] = [1, 1, 2, 3, 5];
fibonacci3.push('8'); //Argument of type '"8"' is not assignable to parameter of type 'number'.
6.2 数组泛型
我们也可以使用
数组泛型
(Array Generic)**
Array<elemType>
**来表示数组
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
6.3 用接口表示数组
也可以用
接口
来描述数组:
interface NumberArray {
[index: number]: number; //任意属性
}
//NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
虽然接口也可以用来描述数组,但我们一般不会这么做,因为这种方式比前两种复杂多了。
不过有一种情况例外,那就是用它来表示类数组(伪数组)。
6.4 类(伪)数组
-
定义
-
拥有
length
属性,其他属性(索引)为非负整数(对象中的索引会被当作字符串来处理,可以当作是个非负整数串来理解) - 不具有数组所具有的方法
-
伪数组,就像数组一样有
length
属性,也有
0
、
1
、
2
等属性的对象,看起来就像数组一样,但不是数组 -
伪数组是一个
Object
,而真实的数组是一个
Array
-
常见的伪数组:
arguments
-
判断伪数组的方法:可以看看《javascript权威指南》,也可以用
Array.isArrar()
来判断
-
拥有
类数组(Array-like Object)不是数组类型,比如
arguments
:
function sum() {
let args: number[] = arguments; //报错:Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.
}
-
arguments
实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口function sum() { let args: { [index: number]: number; //约束当索引的类型、值的类型必须是数字 length: number; //约束类数组必须存在length属性 callee: Function; //约束类数组存在callee属性 } = arguments; }
-
事实上,常用的类数组都有自己的接口定义,如
IArguments
、
NodeList
、
HTMLCollection
等function sum() { let args: IArguments = arguments; }
-
其中
IArguments
是 TypeScript 中定义好了的类型,它实际上就是:interface IArguments { [index: number]: any; length: number; callee: Function; }
-
其中
-
疑问(还是不理解)
- ”一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集”,但为啥下面的代码不会报错?
interface IArguments { [index: number]: number; length: number; callee: Function; }
-
其他人的回答:
-
任意属性的类型为
string
时
,那么确定属性和可选属性的类型都必须为它的类型的子集。 -
number
类型的任意属性
签名不会影响其他 string 类型的属性签名 -
两种任意类型签名并存时,
number
类型的签名指定的值类型必须是
string
类型的签名指定的值类型的子集。
-
/** 虽然指定了 number 类型的任意属性的类型是 string, 但 length 属性是 string 类型的签名,所以不受前者的影响。 **/ type Arg = { [index: number]: string length: number } /** 如果接口定义了 string 类型的任意属性签名, 它不仅会影响其他 string 类型的签名,也会影响其他 number 类型的签名。 **/ interface Person { name: string; age?: number; [propName: string]: string; //任意属性的值允许string类型的属性 }
6.5 any在数组中的应用
一个比较常见的做法是:用
any
表示数组中允许出现任意类型:
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];
7. 函数的类型
一个函数有输入和输出,要在TS中对其进行约束,需要把输入和输出都考虑到。
7.1 函数定义的两种方式
在JavaScript中,有两种常见的定函数的方式:
- 函数声明
// 函数声明(Function Declaration)
function sum(x, y) {
return x + y;
}
- 函数表达式
// 函数表达式(Function Expression)
let mySum = function (x, y) {
return x + y;
};
7.2 函数声明的类型定义
函数声明的类型定义如下:
function sum(x: number, y: number): number {
return x + y;
}
注意,
输入多余的(或少于要求的)参数,是不被允许的
。
function sum(x: number, y: number): number {
return x + y;
}
sum(1, 2, 3); //输入多余的参数:error TS2346: Supplied parameters do not match any signature of call target.
sum(1); //输入少于要求的参数:error TS2346: Supplied parameters do not match any signature of call target.
7.3 函数表达式的类型定义
-
易错
对一个函数表达式(Function Expression)类型不正确的定义:let mySum = function (x: number, y: number): number { return x + y; };
-
如上代码,可以通过编译。不过事实上,上面的代码只对等号右边的匿名函数进行了类型定义,而等号左边的
muSum
,是通过赋值操作进行类型推论而推断出来的
-
如上代码,可以通过编译。不过事实上,上面的代码只对等号右边的匿名函数进行了类型定义,而等号左边的
-
正确的写法
//手动给mySum添加类型 let mySum: (x: number, y: number) => number = function (x: number, y: number): number { return x + y; };
注意,不要混淆了TS中的
=>
和ES6中的
=>
:
-
在TS的类型定义中,
=>
用来表示函数的定义,左边是输入的类型,需要用括号括起来,右边是输出的类型。 -
在ES6中,
=>
是箭头函数,具体参考
ES6中的箭头函数
。
7.4 用接口定义函数的形状
我们也可以使用
接口
的方式来定义一个函数需要符合的形状:
interface SearchFunc {
(source: string, subString: string): boolean;
}
//函数表达式+接口定义函数的类型
let mySearch: SearchFunc; //对等号左边进行类型限制
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
采用函数表达式+接口定义函数类型的方式时,对等号左边进行类型限制,可以保证以后对函数名赋值时,保证参数个数、参数类型、返回值类型不变。
7.5 函数的参数
7.5.1 可选参数
用
?
表示可选的参数。
//lastName为可选的参数
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
注意:
可选参数必须接在必选参数后面
。
-
换句话说,可选参数后面不允许再出现必选参数:
// 可选参数firstName应该写在最后面 function buildName(firstName?: string, lastName: string) { if (firstName) { return firstName + ' ' + lastName; } else { return lastName; } } let tomcat = buildName('Tom', 'Cat'); let tom = buildName(undefined, 'Tom'); // 报错: error TS1016: A required parameter cannot follow an optional parameter.
7.5.2 参数默认值
在ES6中,我们允许给函数的参数添加默认值,
TS会将添加了默认值的参数识别为可选参数
。
//将添加了默认值的参数lastName识别为可选参数
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
添加了默认值的参数被TS识别为可选参数,但不受【可选参数必须接在必须参数后面】的限制:
//添加了默认值的参数不受【可选参数位置】的约束
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');
默认参数更多用法,参考
ES6中函数参数的默认值
7.5.3 剩余参数
在ES6中,可以使用
...rest
的方式获取函数中的剩余参数(rest参数):
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
});
}
let a: any[] = [];
push(a, 1, 2, 3);
在TS中的写法:
// item一个数组,所以用数组的类型来定义它
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
注意:
rest参数只能是最后一个参数
ES6中的剩余参数详解:
-
定义
-
ES6中引入rest参数(形式为
...变量名
),用于获取函数的多余参数,这样就不需要使用
arguments
对象了 -
rest参数搭配的变量是一个数组(例
...val
),该变量(
val
)将多余的参数放入数组中
-
ES6中引入rest参数(形式为
-
rest参数
与
arguments
对象-
arguments
对象不是数组,而是一个类似数组的对象。为了使用数组的方法,必须使用
Array.from
先将其转为数组 -
rest参数
是一个真正的数组,数组特有的方法都可以使用
-
-
rest参数
和函数的
length
属性-
函数的
length
属性,不包括
rest参数
(function(a) {}).length // 1 (function(...a) {}).length // 0 (function(a, ...b) {}).length // 1
-
函数的
7.6 重载
函数重载:允许一个函数接受不同数量或类型的参数时,作出不同的处理。(类似C++的重载)
-
C++函数重载:函数名相同,函数形参列表不同(函数特征标不同)的一类函数称为函数重载。
- 注意函数重载的依据只有形参列表不同。
注意,TS 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
//函数定义
function reverse(x: number): number;
function reverse(x: string): string;
//函数实现
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
8. 类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型。
8.1 语法
两种声明方式:
-
值 as 类型
- 在tsx语法中使用
-
<类型>值
形如
<Foo>
的语法在tsx中表示的是一个
ReactNode
,在ts中除了表示类型断言外,也可能表示的是一
泛型
。
所以建议大家在使用类型断言时,统一使用
值 as 类型
的语法。
8.2 类型断言的用途
类型断言的常见用途有以下几种:
- 将一个联合类型断言为其中一个类型
- 将一个父类断言为更加具体的子类
- 将任何一个类型断言为any
- 将any断言为一个具体的类型
8.2.1 将一个联合类型断言为其中一个类型
当TS不确定一个联合类型的变量到底是哪个类型的时候,我们
只能访问该联合类型的所有类型中共有的属性或方法
:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function getName(animal: Cat | Fish) {
return animal.name; //访问所有类型中共有的属性
}
-
应用
有时候,我们需要在还不确定类型时候就访问其中一个类型特有的属性或方法:interface Cat { name: string; run(): void; } interface Fish { name: string; swim(): void; } function isFish(animal: Cat | Fish) { if (typeof animal.swim === 'function') { //获取animal.swim会报错 return true; } return false; } /** 报错信息: error TS2339: Property 'swim' does not exist on type 'Cat | Fish'. Property 'swim' does not exist on type 'Cat'. **/
解决方法——使用类型断言,将
animal
断言为
Fish
:function isFish(animal: Cat | Fish) { if (typeof (animal as Fish).swim === 'function') { //将animal断言为Fish return true; } return false; }
注意:类型断言只能够【欺骗】TS编译器,但无法避免运行时的错误,滥用类型断言可能会导致运行时的错误:
-
以下代码在编译时不会报错,但在运行时会报错:
interface Cat { name: string; run(): void; } interface Fish { name: string; swim(): void; } function swim(animal: Cat | Fish) { (animal as Fish).swim(); } const tom: Cat = { name: 'Tom', run() { console.log('run') } }; swim(tom); // 运行时报错 //Uncaught TypeError: animal.swim is not a function`
-
原因是:
(animal as Fish).swim()
这段代码隐藏了
animal
可能为
Cat
的情况,将
animal
直接断言为
Fish
。TS编译器相信了我们的断言,所以在调用
swim()
的时候没有编译错误。但是
swim
函数接受的参数类型是
Cat | Fish
,一旦传入了
Cat
类型的变量,由于
Cat
上没有
swim
方法,就会导致运行时的错误了。
-
原因是:
在使用类型断言时,一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
8.2.2 将一个父类断言为更加具体的子类
当类之间有继承关系时,类型断言也是很常见的:
-
一个小栗子
-
需求:定义一个函数
isApiError
用来判断传入的参数是不是
ApiError
类型 -
实现:
-
为了实现这样一个函数,它的参数类型肯定得是比较抽象的父类
Error
,这样的话这个函数就能接受
Error
或它的子类作为参数 -
但是由于父类
Error
中没有
code
属性,故直接获取
error.code
会报错,需要使用类型断言获取
(error as ApiError).code
class ApiError extends Error { code: number = 0; } class HttpError extends Error { statusCode: number = 200; } function isApiError(error: Error) { if (typeof (error as ApiError).code === 'number') { return true; } return false; }
-
为了实现这样一个函数,它的参数类型肯定得是比较抽象的父类
-
需求:定义一个函数
-
一些小思考
-
上面的栗子使用
instanceof
更加合适,因为
ApiError
是一个JS的类,能够通过
instanceof
来判断
error
是否是它的实例。 -
但有的情况下,
ApiError
和
HttpError
不是一个真正的类,而只是一个TS的接口,
接口是一个类型,不是一个真正的值,它在编译结果中会被删除,无法使用
instanceof
来做运行时的判断
function isApiError(error: Error) { if (error instanceof ApiError) { return true; } return false; } // 报错: error TS2693: 'ApiError' only refers to a type, but is being used as a value here.
-
8.2.3 将任何一个类型断言为any
一个小栗子:
-
我们的需求:给
window
添加一个属性
foo
,我们写的代码如下:window.foo = 1; // 报错啦: - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
-
报错提示
window
上不存在属性
foo
-
报错提示
-
解决方法:使用
as any
临时将
window
断言为
any
类型:(window as any).foo = 1;
在
any
类型的变量上,访问任何属性都是允许的。
需要注意的是:将一个变量断言为
any
可以说是解决TS类型问题的最后一个手段。
总之,一方面不能滥用
as any
,另一方面也不要完全否认它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡。
8.2.4 将any断言为一个具体的类型
遇到
any
类型的变量时,我们可以选择无视他,任由它滋生出更多的
any
,也可以选择改进它,通过类型断言及时的把
any
断言为精确的类型。
举个栗子:
-
历史遗留的代码中有个
getCacheData
,它的返回值是
any
:function getCacheData(key: string): any { return (window as any).cache[key]; }
-
那我们在使用它时,最好能够在调用它之后,将它的返回值断言成一个精确的类型,这样就方便了后续的操作:
function getCacheData(key: string): any { return (window as any).cache[key]; } interface Cat { name: string; run(): void; } // 我们调用完 getCacheData 之后,立即将它断言为 Cat 类型 const tom = getCacheData('tom') as Cat; tom.run();
8.3 类型断言的限制
8.4 双重断言
写在最前面——若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。
除非迫不得已,千万别用双重断言
既然,任何类型都可以被断言为any,any可以被断言为任何类型,那我们是不是可以
用双重断言来将任何一个类型断言为任何另一个类型
举个栗子:
interface Cat {
run(): void;
}
interface Fish {
swim(): void;
}
function testCat(cat: Cat) {
return (cat as any as Fish); //如果直接使用 cat as Fish 肯定会报错,因为 Cat 和 Fish 互相都不兼容
8.5 类型断言 VS 类型转换
-
类型断言只会影响TS编译时的类型,类型断言语句在编译结果中会被删除
-
类型断言不是类型转换,它不会真的影响到变量的类型
-
几个栗子:
-
下面的代码,将
something
断言为
boolean
,虽然可以通过编译,但是并没有什么用
function toBoolean(something: any): boolean { return something as boolean; } toBoolean(1); // 返回值为 1
编译后的:
function toBoolean(something) { return something; } toBoolean(1); //类型断言不会真的影响变量的类型 // 返回值为 1
- 类型转换
function toBoolean(something: any): boolean { return Boolean(something); } toBoolean(1); //浅浅做个对比 // 返回值为 true
-
下面的代码,将
8.6 类型断言 VS 类型声明
举个栗子说说类型断言和类型声明的区别:
-
同——两者都能达到的效果:
-
使用
as Cat
将
any
类型断言为了
Cat
类型function getCacheData(key: string): any { return (window as any).cache[key]; } interface Cat { name: string; run(): void; } const tom = getCacheData('tom') as Cat; tom.run();
- 使用类型声明也能达到上面的效果
-
使用
//将 tom 声明为 Cat,然后再将 any 类型的 getCacheData('tom') 赋值给 Cat 类型的 tom
const tom: Cat = getCacheData('tom');
tom.run();
-
异——两者的区别
- 使用类型断言
interface Animal { name: string; } interface Cat { name: string; run(): void; } const animal: Animal = { name: 'tom' }; // 由于 Animal 兼容 Cat,故可以将 animal 断言为 Cat 赋值给 tom let tom = animal as Cat;
- 使用类型声明(会报错)
//报错原因:Animal 可以看作是 Cat 的父类,当然不能将父类的实例(animal)赋值给类型为子类(Cat)的变量 let tom: Cat = animal; // - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.
-
这个栗子中,类型断言和类型声明的核心区别:
-
animal
断言为
Cat
,只需要满足
Animal
兼容
Cat
或
Cat
兼容
Animal
即可 -
animal
赋值给
tom
,需要满足
Cat
兼容
Animal
才行 -
在前一个栗子中,由于
getCacheData('tom')
是
any
类型,
any
兼容
Cat
,
Cat
也兼容
any
,所以
const tom = getCacheData('tom') as Cat; //等价于 //const tom: Cat = getCacheData('tom');
-
通过上面的例子,我们知道了类型声明比类型断言更加严格,所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的
as
语法更优雅。
8.7 类型断言 VS 泛型
9. 声明文件
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
9.1 新语法
-
declare var
声明全局变量 -
declare function
声明全局方法 -
declare class
声明全局类 -
declare enum
声明全局枚举类型 -
declare namespace
声明(含有子属性的)全局对象 -
interface
和
type
声明全局类型 -
export
导出变量 -
export namespace
导出(含有子属性的)对象 -
export default
ES6 默认导出 -
export =
commonjs 导出模块 -
export as namespace
UMD 库声明全局变量 -
declare global
扩展全局变量 -
declare module
扩展模块 -
/// <reference />
三斜线指令
9.2 什么是声明语句
假如我们想要使用第三方库jQuery,一种常见的方式是在html中通过
<script>
标签引入jQuery,然后就可以使用全局变量
$
或
jQuery
了。
我们通常这样获取一个
id
为
foo
的元素:
$('#foo');
// or
jQuery('#foo');
但在TS中,编译器并不知道
$
或
jQuery
是什么:
jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.
这时,我们需要使用
declare var
来定义它的类型:
declare var jQuery: (selector: string) => any;
jQuery('#foo');
注意
:上例中,
declare var
并没有真的定义一个变量,只是定义了全局变量
jQuery
的类型,仅仅会用于编译时的检查,在编译结果中会被删除。
上例的编译结果为:
jQuery('#foo');
除了
declare var
之外,还有其他很多种声明语句,将会在后面详细介绍。
9.3 什么是声明文件
- 通常我们会把声明语句放到一个单独的文件中,这就是声明文件
-
声明文件必须以
.d.ts
为后缀
一般来说,ts会解析项目中所有的
*.ts
文件,当然也包括
.d.ts
结尾的文件。加入无法解析,那么可以检查下
tsconfig.json
中的
files
、
include
和
exclude
配置,确保其包含了
.d.ts
文件。
9.3.1 第三方声明文件
推荐使用
@types
统一管理第三方库的声明文件。
@types
的使用方式很简单,直接使用
npm
安装对应的声明模块即可。
以jQuery举例:
npm install @types/jquery --save-dev
9.4 书写声明文件
当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。
在不同的场景下,声明文件的内容和使用方式会有所区别。
库的使用场景主要有以下几种:
-
全局变量:通过
<script>
标签引入第三方库,注入全局变量 -
npm 包:通过
import foo from 'foo'
导入,符合 ES6 模块规范 -
UMD 库:既可以通过
<script>
标签引入,又可以通过
import
导入 -
直接扩展全局变量:通过
<script>
标签引入后,改变一个全局变量的结构 -
在npm 包或 UMD库中扩展全局变量:引用
npm
包或
UMD
库后,改变一个全局变量的结构 -
模块插件:通过
<script>
或
import
导入后,改变另一个模块的结构
9.4.1 全局变量
9.4.2 npm 包
9.4.3 UMD 库
9.4.4 直接扩展全局变量
9.4.5 在npm 包或 UMD库中扩展全局变量
9.4.6 模块插件
9.4.7 声明文件中的依赖
9.4.8 自动生成声明文件
9.5 发布声明文件
9.5.1 将声明文件和源码放在一起
9.5.2 将声明文件发布到@types 下
10. 内置对象
JS中有很多
内置对象
,它们可以直接在TS中当作定义好了的类型。
内置对象是根据标准在全局作用域(Global)上存在的对象。
这里的标准是指ECMAScript 和其他环境(比如 DOM)的标准。
10.1 ECMAScript 的内置对象
ECMAScript 标准提供的内置对象有:
Boolean
、
Error
、
Date
、
RegExp
等,更多的内置对象,可以查看
MDN的文档
。
我们可以在TS中将变量定义为这些类型:
let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;
而他们的定义文件,则在
TypeScript 核心库的定义文件
中。
10.2 DOM和BOM的内置对象
DOM 和 BOM 提供的内置对象有:
Document
、
HTMLElement
、
Event
、
NodeList
等。
TS中会经常用到这些类型:
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});
它们的定义文件同样在
TypeScript 核心库的定义文件
中。
10.3 TypeScript核心库的定义文件
TypeScript 核心库的定义文件
定义了所有浏览器环境需要用到的类型,并且是预置在TS中的。
当我们在使用一些常用的方法时,TS实际上已经帮我们做了很多类型判断的工作了,
-
比如:
// Math.pow 必须接受两个 number 类型的参数 Math.pow(10, '2'); // error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
事实上,
Math.pow
的类型定义如下:interface Math { /** * Returns the value of a base expression taken to a specified power. * @param x The base value of the expression. * @param y The exponent value of the expression. */ pow(x: number, y: number): number; }
注意,TypeScript 核心库的定义中不包含 Node.js 部分。
10.4 用 TypeScript 写 Node.js
Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:
npm install @types/node --save-dev
三、进阶
1. 类型别名
类型别名用来给一个类型起个新名字
。
举个例子:
type Name = string;
type NameResolver = () => string; // => 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型
type NameOrResolver = Name | NameResolver; //类别别名常用于联合类型
function getName(n: NameOrResolver): Name { // 接收一个函数为参数
if (typeof n === 'string') {
return n;
} else {
return n();
}
}
2. 字符串字面量类型
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
注意:
类别别名与字符串字面量类型都是使用
type
来定义
。
一个例子:
// 定义一个字符串字面量类型,只能取三种字符串中的一种
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'
// error TS2345: Argument of type '"dblclick"' is not assignable to parameter of type 'EventNames'.
事实上不一定要字符串,任意基础类型都是可以的。
例如:
type Nums = 1 | 2 | 3;
- 一个疑问:和枚举的区别?
3. 元组
数组合并了相同类型的对象,而
元组(Tuple)合并了不同类型的对象
。
元组用于保存定长数据类型的数据。
3.1 一个例子
-
定义一个元组
定义一对值分别为
string
和
number
的元组:// 类型必须一一匹配,且个数必须为2 let tom: [string, number] = ['Tom', 25];
-
赋值或访问元组
-
当赋值或访问一个已知索引的元素时,会得到正确的类型
let tom: [string, number]; tom = ['Jack',18]; //要赋值才能用,否则会编译错误 tom[0] = 'Tom'; tom[1] = 25; tom[0].slice(1); tom[1].toFixed(2);
-
也可以只赋其中一项
let tom: [string, number]; tom[0] = 'Tom';
-
但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项
let tom: [string, number]; tom = ['Tom', 25];
let tom: [string, number]; tom = ['Tom']; //报错 // Property '1' is missing in type '[string]' but required in type '[string, number]'.
-
3.2 越界的元素
虽然可以越界添加元素(不建议),但是不能越界访问,越界访问会提示错误。
当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:
let tom: [string, number];
tom = ['Tom', 25];
tom.push('male');
tom.push(true);
// Argument of type 'true' is not assignable to parameter of type 'string | number'.
4. 枚举
枚举(Enum)类型用于取值被限定在一定范围内的场景
,比如一周只能有7天,颜色限定为红绿蓝等。
4.1 简单的例子
-
枚举使用
enum
关键字来定义:enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
-
枚举成员会被赋值为从
0
开始递增的数字,同时也会对枚举值到枚举名进行反向映射:enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; // 枚举成员会被赋值为从`0`开始递增的数字 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
事实上,上面的例子会被编译为:
var Days; (function (Days) { Days[Days["Sun"] = 0] = "Sun"; Days[Days["Mon"] = 1] = "Mon"; Days[Days["Tue"] = 2] = "Tue"; Days[Days["Wed"] = 3] = "Wed"; Days[Days["Thu"] = 4] = "Thu"; Days[Days["Fri"] = 5] = "Fri"; Days[Days["Sat"] = 6] = "Sat"; })(Days || (Days = {}));
4.2 手动赋值
我们也可以
给枚举类型手动赋值,未手动赋值的枚举项会接着上一个枚举项递增
:
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
如果未手动赋值的枚举项与手动赋值的枚举项重复了,TS是不会察觉到这一点的:
enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 3); // true
// 递增到 3 的时候与前面的 Sun 的取值重复了,但是 TypeScript 并没有报错
console.log(Days["Wed"] === 3); // true
// 导致 Days[3] 的值先是 "Sun",而后又被 "Wed" 覆盖了
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true
/**
编译的结果:
var Days;
(function (Days) {
Days[Days["Sun"] = 3] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));
所以使用的时候需要注意,最好不要出现这种覆盖的情况。
*/
几个点:
- 手动赋值的枚举项可以不是数字,此时需要使用类型断言来让tsc无视类型检查。
-
手动赋值的枚举项也可以为小数或负数,此时后续为手动赋值的项的递增步长仍为
1
:enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat}; console.log(Days["Sun"] === 7); // true console.log(Days["Mon"] === 1.5); // true console.log(Days["Tue"] === 2.5); // true console.log(Days["Sat"] === 6.5); // true```
4.3 常数项和计算所得项
枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。
前面我们所举的例子都是常数项,一个典型的计算所得项的例子来了:
//"blue".length 就是一个计算所得项
enum Color {Red, Green, Blue = "blue".length};
上面的例子不会报错,但
如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错
:
// 紧接在计算所得项后面的是未手动赋值的项Green,Green因为无法获得初始值而报错
enum Color {Red = "red".length, Green, Blue};
//error TS1061: Enum member must have initializer.
// error TS1061: Enum member must have initializer.
当满足以下条件时,枚举成员被当作是常数:
-
不具有初始化函数,并且之前的枚举成员是常数
-
在这种情况下,当前枚举成员的值为上一个枚举成员的值加
1
。但第一个枚举元素是个例外,如果它没有初始化方法,那么它的初始值为
0
-
在这种情况下,当前枚举成员的值为上一个枚举成员的值加
-
枚举成员使用常数枚举表达式初始化
常数枚举表达式是TS表达式的子集,它可以在编译阶段求值。当一个表达式满足下面的条件之一时,它就是一个常数枚举表达式:- 数字字面量
- 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的),如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用
- 带括号的常数枚举表达式
-
+
、
-
、
~
一元运算符应用于常数枚举表达式 -
+
,
-,
*
,
/
,
%
,
<<
,
>>
,
>>>
,
&
,
|
,
^
二元运算符,常数枚举表达式作为其一个操作对象。若常数枚举表达式求值后为
NaN
或
Infinity
,则会在编译阶段报错
所有其他的情况,枚举成员都被当作是计算得出的值。
4.4 常数枚举
常数枚举是使用
const enum
定义的枚举类型:
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
常数枚举于普通枚举的区别:
常数枚举会在编译阶段被删除,并且不能包含计算成员
。
上例的编译结果是:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
假如包含了计算成员,则会在编译阶段报错:
const enum Color {Red, Green, Blue = "blue".length};
// error TS2474: In 'const' enum declarations member initializer must be constant expression.
4.5 外部枚举
外部枚举是使用
declare enum
定义的枚举类型:
declare enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
declare
定义的类型只会用于编译时的检查,编译结果中会被删除。
上例的编译结果:
// 注意看看编译结果和之前有啥不同
var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
外部枚举与声明语句一样,常出现在声明文件中。
同时使用
declare
和
const
也是可以的:
declare const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
编译结果:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
5. 类
传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。
在 ES6 中,我们终于迎来了 class。
5.1 类的概念
- 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
-
对象(Object):类的实例,通过
new
生成 -
面向对象(OOP)的三大特性:封装、继承、多态
- 封装(Encapsulation):对数据的操作细节隐藏起来,只暴露对外的接口。外界调用不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
- 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些自己更具体的特性
- 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法
- 存取器(getter & setter):用以改变属性的读取和赋值行为
- 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有的
- 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
- 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现。一个类只能继承自另一个类,但是可以实现多个接口
5.2 ES6中类的用法
5.2.1 属性和方法
使用
class
定义类,使用
constructor
定义构造函数。
通过
new
生成新实例的时候,会自动调用构造函数。
class Animal {
public name;
constructor(name) {
this.name = name;
}
sayHi() {
return `My name is ${this.name}`;
}
}
let a = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack
5.2.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
5.2.3 存取器
使用
getter
和
setter
可以改变属性的赋值和读取行为
class Animal {
constructor(name) {
this.name = name;
}
get name() {
return 'Jack';
}
set name(value) {
console.log('setter: ' + value);
}
}
let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // getter:Jack
5.2.4 静态方法
使用
static
修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用。
class Animal {
static isAnimal(a) {
return a instanceof Animal;
}
}
let a = new Animal('Jack');
//直接通过类来调用
Animal.isAnimal(a); // true
//不能这么调用
a.isAnimal(a); // TypeError: a.isAnimal is not a function
5.3 ES7中类的用法
ES7 中有一些关于类的提案,TypeScript 也实现了它们,这里做一个简单的介绍。
5.3.1 实例属性
ES6 中实例的属性只能通过构造函数中的
this.xxx
来定义,ES7 中可以直接在类里面定义:
class Animal {
name = 'Jack'; //直接在类中定义
constructor() {
// ...
}
}
let a = new Animal();
console.log(a.name); // Jack
5.3.2 静态属性
可以使用
static
定义一个静态属性:
class Animal {
static num = 42;
constructor() {
// ...
}
}
// 也是通过类来访问
console.log(Animal.num); // 42
5.4 TS中类的用法
5.4.1 public、private 和 protected
TypeScript 可以使用三种修饰符,分别是
public
、
private
和
protected
:
-
public
修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public -
private
修饰的属性或方法是私有的,不能在声明它的类的外部访问-
如果希望有的属性是无法直接存取的,这时候就可以用
private
-
使用
private
修饰的属性或方法,在子类中也是不允许访问的 -
当构造函数修饰为
private
时,该类不允许被继承或者实例化 -
需要注意的是,TS编译之后的代码中,并没有限制
private
属性在外部的可访问性class Animal { private name; public constructor(name) { this.name = name; } } let a = new Animal('Jack'); console.log(a.name); //error TS2341: Property 'name' is private and only accessible within class 'Animal'. a.name = 'Tom'; // error TS2341: Property 'name' is private and only accessible within class 'Animal'. /** 编译后的代码: var Animal = (function () { function Animal(name) { this.name = name; } return Animal; })(); var a = new Animal('Jack'); console.log(a.name); a.name = 'Tom'; **/
-
如果希望有的属性是无法直接存取的,这时候就可以用
-
protected
修饰的属性或方法是受保护的,它和
private
类似,区别是它在子类中是允许被访问的-
当构造函数修饰为
protected
时,该类只允许被继承
-
当构造函数修饰为
5.4.2 参数属性
修饰符和
readonly
还可以使用在构造函数参数中,等价于类中定义该属性并赋值,使代码更加简洁。
class Animal {
// public name: string;
public constructor(public name) {
// this.name = name;
}
}
5.4.3 readonly
只读属性关键字,只允许出现在属性声明、索引签名或构造函数中。
class Animal {
readonly name; //属性声明
public constructor(name) {
this.name = name;
}
}
let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom'; //不能给只读属性赋值: TS2540: Cannot assign to 'name' because it is a read-only property.
注意:如果
readonly
和其他访问修饰符同时存在的话,需要写在其后面。
class Animal {
// public readonly name;
public constructor(public readonly name) {
// this.name = name;
}
}
5.4.4 抽象类
abstrct
用于定义抽象类和其中的抽象方法。
什么是抽象类?
- 抽象类不允许被实例化
- 抽象类中的抽象方法必须被子类实现
- 机使是抽象方法,TS的编译结果中,仍然会存在这个类
看看一个小例子把:
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}
class Cat extends Animal {
public sayHi() {
console.log(`Meow, My name is ${this.name}`);
}
}
let cat = new Cat('Tom');
5.5 类的类型
给类加上TS的类型,与接口类似:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
sayHi(): string {
return `My name is ${this.name}`;
}
}
let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack
6. 类与接口
7. 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而是在使用的时候再指定类型的一种特性。
7.1 一个简单的例子
我们的需求:实现一个函数
createArray
,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值
-
使用使用之前提到过的数组泛型来定义返回值的类型
// Array<any>允许数组的每一项都为任意类型 function createArray(length: number, value: any): Array<any> { let result = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; } createArray(3, 'x'); // ['x', 'x', 'x']
- 上面的代码编译不会报错,但是有一个缺陷:它没有准确的定义返回值的类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。
-
使用泛型
//T 用来指代任意输入的类型 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']
- 接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来
7.2 多个类型参数
定义泛型的时候,可以一次定义多个类型参数:
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // ['seven', 7]
7.3 泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // 泛型 T 不一定包含属性 length,所以编译的时候报错
return arg;
}
// error TS2339: Property 'length' does not exist on type 'T'.
解决方法: 我们可以
对泛型进行约束
,只允许这个函数传入那些包含
length
属性的变量,这就是泛型约束:
-
使用
extends
约束了泛型
T
必须符合接口的形状
,也就是必须包含
length
属性
interface Lengthwise {
length: number;
}
// 使用extends约束泛型T
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
// 如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错
loggingIdentity(7);
// 报错:error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.
多个类型参数之间也可以互相约束:
// 使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id]; // <T>source 就是 source as T,把 source 断言成 T 类型
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 });
7.4 泛型接口
之前学习过,可以使用接口的方式来定义一个函数需要符合的形状:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
也可以使用含有泛型的接口来定义函数的形状:
interface CreateArrayFunc {
<T>(length: number, value: T): Array<T>;
}
let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
进一步,可以把泛型参数提前到接口上:
注意
,此时在使用泛型接口的时候,需要定义泛型的类型。(想想C++是咋写的)
interface CreateArrayFunc<T> {
(length: number, value: T): Array<T>;
}
// 在使用泛型接口的时候,需要定义泛型的类型
let createArray: CreateArrayFunc<any>;
7.5 泛型类
与泛型接口类似,泛型也可以用于类的类型定义中:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
// 同样要需要定义泛型的类型
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
/**
关于报错:·Property 'zeroValue' has no initializer and is not definitely assigned in the constructor.ts(2564)`
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
可以改成:
class GenericNumber<T> {
// to get rid of error, you can define constructor
// which takes [zeroValue] and [add] as arguments
constructor(public zeroValue: T, public add: (x: T, y: T) => T){
this.zeroValue = zeroValue;
this.add = add;
}
}
**/
7.6 泛型参数的默认类型
在TS 2.3 以后,我们可以为泛型中的类型参数指定默认类型。
当使用泛型时没有在代码中直接指定类型参数,从实际参数中也无法推测出时,这个默认类型就会起作用。
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
8. 声明合并
9. 扩展阅读
四、工程
1. 代码检查
2. 编译选项