介绍

TypeScript是静态类型的弱类型语言,在编译阶段就能确定每个变量的类型,编译器可以进行类型检查并且会标注出语法错误。

弱类型:因为完全兼容Javascript,所以同样也是弱类型,即可以进行隐式类型转换,如:

1
2
console.log(1 + '1');
// 打印出字符串 '11'

但我们可以通过声明变量的类型,将Ts作为一个强类型语言来使用。

增强了IED的功能,提供代码补全等能力。

数据类型

变量声明方式

1
let num: number = 1;

注意,如果使用Number()去创建一个变量,这个变量并不是number类型,而是Number类型。前者是基本数据类型,而后者是一个构造函数对象。

空值

可以指定一个函数的返回值为空值(void

1
2
3
function test(): void {
//do something
}

可以将指定变量类型为void,此时只能将这个变量赋值为undefined和null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

### void与never的区别

never是永远不存在的类型,比如一个函数总是`抛出错误`,而没有返回值。或者一个函数内部有`死循环`,永远不会有返回值。函数的返回值就是never类型。

## 任意值

被声明为任意值(Any)的:变量允许被赋值为任何类型,并且对它的任何操作,返回的内容类型都是人任意值。

后端需要把数据导过来

如果在声明对象时,没有指定其类型,就相当于定义了一个任意值类型,如:

```typescript
let r;
r = '1';
r = 1;

等价于

1
2
3
let r:any;
r = '1';
r = 1;

any和unkown的区别

unknown类型和any类型类似。与any类型不同的是。unknown类型可以接受任意类型赋值,但是unknown类型赋值给其他类型前,必须被断言

类型推论

上面已经说过,声明对象时,如果没有指定其类型,就相当于定义了一个任意值类型,这其实就是类型推断的一个机制。

Ts会在未指定变量类型的时候,给变量一个推断的类型,如:

1
2
let n = 123;
let f = "string"

相当于

1
2
let n: number = 123
let f: string = "string"

联合类型

联合类型表示取值可以为多种类型中的一种,类型之间用|分隔,比如:

1
let n: number | string = 1

访问联合属性的属性和方法时,如果不确定变量到底是联合类型中的哪个类型,我们就只能访问到此联合类型所有类型的属性和方法。

1
2
3
4
5
6
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'.

如上例,因为length不是StringNumber的共有方法,所以会报错,而:

1
2
3
function getString(something: string | number): string {
return something.toString();
}

是可以访问的,因为toStringStringNumber的共有方法。

联合变量在被赋值时会执行类型推论。

类型别名

使用type关键字创建类型别名。常用于联合类型。

1
2
3
4
5
6
7
8
9
10
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来定义,约束取值只能是某几个字符串中的一个。

1
2
3
4
5
6
7
8
9
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'.

接口

接口(interface)是对象的类型,是对行为的抽象。而具体的就要靠类(class)去实现(implement)。

Ts中的接口除了可以对类的一部分行为进行抽象,还可以用来对对象的形状进行描述。

1
2
3
4
5
6
7
8
9
interface Person {
name: string;
age: number;
}

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

接口一般首字母大写 ,推荐使用I开头。

可选属性

对象的属性必须与接口一致,既不能多也不能少,所以如果需要指定可选属性,可以在接口的属性名后面加上?,那么这样的赋值就是允许的:

1
2
3
4
5
6
7
8
interface Person {
name: string;
age?: number;
}

let tom: Person = {
name: 'Tom'
};

扩展:TS中?的其他用法

可选链(.?):

传入的data参数可能为null,undefined,通常我们的写法是直接上if判断,然后再取data中的属性,但是有了问号点(?.)写法就简单很多了,看下面例子:

1
2
3
4
5
//1.data可能为null,undefined , row也可能为null,undefined
//2.假设data完整结构 {row:{name:'aaa'}}
function getData(data: any){
let name = data?.row?.name
}

等价于

1
2
3
4
5
6
7
8
//1.data可能为null,undefined , row也可能为null,undefined
//2.假设data完整结构 {row:{name:'aaa'}}
function getData(data: any){
let name;
if (data && data.row) {
name = data.row.name
}
}

从上面写法可以看出来问号点(?.)的写法其实等价于例2的if判断、三元运算符(let a = b == null ? null : b.a)

空值合并操作符(??)

我们想判断一个值存在,一般需要考虑0的情况,比如:

1
2
3
4
5
6
7
8
let a = 0;
let c = { name:'yyqx' }

if(!!a || a === 0 ){
b = a;
}else{
b = c;
}

使用??的简化写法:

1
2
3
4
5
let b;
let a = 0;
let c = { name:'yyqx' }

b = a ?? c;

这样写的含义是:当a为非空值(非undefined、null)时b=a,否则b=c。

空值赋值运算符(??=)

1
2
3
4
5
6
let b = 'hello';
let a = 0
let c = null;
let d = ’123
b ??= a; // b = “hello”
c ??= d // c = '123'

当??=左侧的值为null、undefined的时候,才会将右侧变量的值赋值给左侧变量。

!的用法

!不可为空的成员

1
2
3
4
5
6
7
8
// 这里的?表示这个name属性有可能不存在
class A {
name?: string
}

interface B {
name?: string
}

!.在变量名后添加!,可以断言排除undefined和null类型。

1
2
3
let a: string | null | undefined
a.length // error
a!.length // ok

!:,待会分配这个变量。

任意属性

也叫做可索引类型

如果希望一个接口允许有任意属性,可以加上[propName:类型]:any。其中propName类似形参,名字可以任意指定。

1
2
3
4
5
6
7
8
9
10
interface Person {
name: string;
age?: number;
[propName: string]: any;
}

let tom: Person = {
name: 'Tom',
gender: 'male'
};

先看官方文档解释:可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。

则[propName: string] : any就是描述了对象索引的类型为string,索引返回值类型为any。

再通俗一点来讲,[propName: string] : any 就表示键为string类型,值为any类型的属性。

多个任意属性存在的情况

索引签名可以使用字符串和数字两种类型的索引。

不同类型的索引可以同时声明。但是当两种类型索引同时存在时,number类型索引的返回值类型必须是string类型索引的返回值类型的子类型

官方解释:当使用number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

相同类型的索引不能同时声明。如果接口中给有多个类型属性,则可以在任意属性中使用联合属性:

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
age?: number;
[propName: string]: string | number;
}

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

存在任意属性和其他属性的情况

注意:一旦定义了任意类型属性,那么确定属性和可选属性的类型都必须是它的类型的子集。

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
age?: number;
[propName: string]: string;
}

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

如上,有一个任意类属性规定类型为string,可选属性age类型为number,不是string的子类,所以会报错。

当索引的类型是number时,则有特殊的情况:其他属性如果索引类型为string,则此索引返回值类型不会受前者索引返回值类型的影响,如:

1
2
3
4
type Arg = {
[index: number]: number;
length: string;
};

虽然有任意属性和其他属性同时存在,但length的索引类型为string,所以不会受index的影响,此声明是合理的。

而:

1
2
3
4
5
type MyArray = {
0: string;
[index: number]: number;
};
// Property '0' of type 'string' is not assignable to numeric index type 'number'.

因为0是number类型的索引,所以会受到index的影响。

一个接口是可以同时定义两种任意属性的。但number类型的属性值类型必须是string类型的属性值类型的子集。如:

1
2
3
4
5
interface C {
[prop: string]: number;
[index: number]: string;
}
// Numeric index type 'string' is not assignable to string index type 'number'.

因为index指定的值类型是string,不是prop的值类型number的子集,所以会报错。而

1
2
3
4
interface C {
[prop: string]: object;
[index: number]: Function;
}

则是成立的。因为Function是object的子集。

只读属性

属性前面使用readonly关键字,一旦赋值,不可以被改写。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.

报错信息有两处,第一处是在对 tom 进行赋值的时候,没有给 id 赋值。

第二处是在给 tom.id 赋值的时候,由于它是只读属性,所以报错了。

数组

数组有多种表示方法。

变量名:类型[]表示

1
let arr: number[] = [1,2,3,4,5];

数组泛型表示

1
let arr: Array<number> = [1,2,3,4,5];

接口表示

1
2
3
4
interface numArray {
[index: number]:number;
}
let arr: numArray = [1,2,3,4,5]

约束了索引类型为number类型时,对应的值类型也必须是number类型。

虽然这种方式可以用来描述数组,但是一般使用前两种方式,声明比较简单。但是可以用来表示类数组。

类数组

类数组不是数组类型,比如arguments,不能用第一、二中方式来表示。

1
2
3
4
5
function sum() {
let args: number[] = arguments;
}

// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.

应该使用接口方式来表示:

1
2
3
4
5
6
7
function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}

类数组有自己的接口定义,比如IArguments等。

any在数组中的应用

any允许数组中出现任意类型。

let arr: any[] = [1,’2’,{}]

元组

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

示例

1
let tom: [string, number] = ['Tom', 25];

当赋值或访问一个已知索引的元素时,会得到正确的类型:

1
2
3
4
5
6
let tom: [string, number];
tom[0] = 'Tom';
tom[1] = 25;

tom[0].slice(1);
tom[1].toFixed(2);

也可以只赋值其中一项:

1
2
let tom: [string, number];
tom[0] = 'Tom';

但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。

1
2
let tom: [string, number];
tom = ['Tom', 25];
1
2
3
4
let tom: [string, number];
tom = ['Tom'];

// Property '1' is missing in type '[string]' but required in type '[string, number]'.

越界的元素

当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:

1
2
3
4
5
6
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'.

枚举

枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

枚举使用 enum 关键字来定义:

1
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射

1
2
3
4
5
6
7
8
9
10
11
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

事实上,上面的例子会被编译为:

1
2
3
4
5
6
7
8
9
10
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 = {}));

手动赋值

我们也可以给枚举项手动赋值:

1
2
3
4
5
6
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
2
3
4
5
6
enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 3); // true
console.log(Days["Wed"] === 3); // true
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true

相当于编译成

1
2
3
4
5
6
7
8
9
10
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 无视类型检查 (编译出的 js 仍然是可用的):

1
2
3
4
5
6
7
8
9
10
11
enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat = <any>"S"};
var Days;
(function (Days) {
Days[Days["Sun"] = 7] = "Sun";
Days[Days["Mon"] = 8] = "Mon";
Days[Days["Tue"] = 9] = "Tue";
Days[Days["Wed"] = 10] = "Wed";
Days[Days["Thu"] = 11] = "Thu";
Days[Days["Fri"] = 12] = "Fri";
Days[Days["Sat"] = "S"] = "Sat";
})(Days || (Days = {}));

当然,手动赋值的枚举项也可以为小数或负数,此时后续未手动赋值的项的递增步长仍为 1

1
2
3
4
5
6
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

常数项和计算所得项

枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。

前面我们所举的例子都是常数项,一个典型的计算所得项的例子:

1
enum Color {Red, Green, Blue = "blue".length};

上面的例子中,"blue".length 就是一个计算所得项。

上面的例子不会报错,但是如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错

1
2
3
4
enum Color {Red = "red".length, Green, Blue};

// index.ts(1,33): error TS1061: Enum member must have initializer.
// index.ts(1,40): error TS1061: Enum member must have initializer.

所以计算所得项一定要写在未赋值项的最后

注意:使用了+, -, ~ 一元运算符,+, -, *, /, %, <<, >>, >>>, &, |, ^ 二元运算符,也算是常数表达式。若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错。

常数枚举

常数枚举是使用 const enum 定义的枚举类型:

1
2
3
4
5
6
7
8
const enum Directions {
Up,
Down,
Left,
Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。

上例的编译结果是:

1
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

假如包含了计算成员,则会在编译阶段报错:

1
2
3
const enum Color {Red, Green, Blue = "blue".length};

// index.ts(1,38): error TS2474: In 'const' enum declarations member initializer must be constant expression.

函数

声明方式

对比js中函数有两种定义的方式:函数声明函数表达式

1
2
3
4
5
6
7
8
9
// 函数声明(Function Declaration)
function sum(x, y) {
return x + y;
}

// 函数表达式(Function Expression)
let mySum = function (x, y) {
return x + y;
};

Typescript的函数声明法

1
2
3
function sum(x:number,y:number):number {
return x+y;
}

对输入输入的类型进行限制。调用函数时,参数的数量必须与形参匹配,否则会报错。

Typescript的函数表达式法

如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样:

1
2
3
let mySum = function (x: number, y: number): number {
return x + y;
};

这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum 添加类型,则应该是这样:

1
2
3
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};

注意区分ES6的箭头函数。在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

利用接口定义函数形状

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}

保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。

可选参数

与接口中的可选属性类似,我们用 ? 表示可选的参数:

1
2
3
4
5
6
7
8
9
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了

1
2
3
4
5
6
7
8
9
10
11
function buildName(firstName?: string, lastName: string) {
if (firstName) {
return firstName + ' ' + lastName;
} else {
return lastName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName(undefined, 'Tom');

// index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.

参数默认值

在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数

1
2
3
4
5
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

此时就不受「可选参数必须接在必需参数后面」的限制了:

1
2
3
4
5
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

剩余参数

类似于ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(ret参数):

1
2
3
4
5
6
7
8
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
});
}

let a: any[] = [];
push(a, 1, 2, 3);

事实上,items 是一个数组。在TS中我们可以用数组的类型来定义它:

1
2
3
4
5
6
7
8
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}

let a = [];
push(a, 1, 2, 3);

注意,rest 参数只能是最后一个参数

重载

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

比如我们要实现字符串或者数字的反转,联合类型的实现方式是:

1
2
3
4
5
6
7
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('');
}
}

但是这样写无法将返回结果和参数进行对应,即输入字符串应返回字符串,输入数字应返回数字。

这时就可以用重载去定义多个函数类型。

1
2
3
4
5
6
7
8
9
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('');
}
}

原理是,TS会优先从前面的函数开始匹配,即前面应该写更精确的定义。

类型断言

类型断言可以用来手动指定一个值的类型。

写法是

1
as 类型

1
<类型>值

尽量使用前者as的语法,第二种可能遇到不支持的情况。

类型断言的用途

将一个联合类型断言为其中一个类型

因为前面当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法

1
2
3
4
5
6
7
8
9
10
11
12
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}

function getName(animal: Cat | Fish) {
return animal.name;
}

而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}

function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') {
return true;
}
return false;
}

// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.

上面的例子中,获取 animal.swim 的时候会报错。

此时可以使用类型断言,将 animal 断言成 Fish

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}

function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}

但是需要注意,类型断言不能滥用。编译器会信任我们的断言,有时候虽然编译时不会报错,但是可能会运行时报错。

将一个父类断言为更加具体的子类

当类之间有继承关系时,类型断言也是很常见的:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

上面的例子中,我们声明了函数 isApiError,它用来判断传入的参数是不是 ApiError 类型,为了实现这样一个函数,它的参数的类型肯定得是比较抽象的父类 Error,这样的话这个函数就能接受 Error 或它的子类作为参数了。

但是由于父类 Error 中没有 code 属性,故直接获取 error.code 会报错,需要使用类型断言获取 (error as ApiError).code

大家可能会注意到,在这个例子中有一个更合适的方式来判断是不是 ApiError,那就是使用 instanceof

1
2
3
4
5
6
7
8
9
10
11
12
13
class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
}

function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}

上面的例子中,确实使用 instanceof 更加合适,因为 ApiError 是一个 JavaScript 的类,能够通过 instanceof 来判断 error 是否是它的实例。

但是有的情况下 ApiErrorHttpError 不是一个真正的类,而只是一个 TypeScript 的接口(interface,接口是一个类型,不是一个真正的值,它在编译结果中会被删除,当然就无法使用 instanceof 来做运行时判断了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}

function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}

// index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.

此时就只能用类型断言,通过判断是否存在 code 属性,来判断传入的参数是不是 ApiError 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}

function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}

将任何一个类型断言为 any

1
2
3
4
const foo: number = 1;
foo.length = 1;

// index.ts:2:5 - error TS2339: Property 'length' does not exist on type 'number'.

上面的例子中,数字类型的变量 foo 上是没有 length 属性的,故 TypeScript 给出了相应的错误提示。

这种错误提示显然是非常有用的。

但有的时候,我们非常确定这段代码不会出错,比如下面这个例子:

1
2
3
window.foo = 1;

// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.

上面的例子中,我们需要将 window 上添加一个属性 foo,但 TypeScript 编译时会报错,提示我们 window 上不存在 foo 属性。

此时我们可以使用 as any 临时将 window 断言为 any 类型:

1
(window as any).foo = 1;

any 类型的变量上,访问任何属性都是允许的。

一方面不能滥用 as any,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡(这也是 TypeScript 的设计理念之一),才能发挥出 TypeScript 最大的价值。

将 any 断言为一个具体的类型

1
2
3
4
5
6
7
8
9
10
11
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();

上面的例子中,我们调用完 getCacheData 之后,立即将它断言为 Cat 类型。这样的话明确了 tom 的类型,后续对 tom 的访问时就有了代码补全,提高了代码的可维护性。

类型断言的限制

是不是任何一个类型都可以被断言为任何另一个类型呢?

并非如此。

具体来说,若 A 兼容 B,那么 A 能够被断言为 BB 也能被断言为 A

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}

let tom: Cat = {
name: 'Tom',
run: () => { console.log('run') }
};
let animal: Animal = tom;

在上面的例子中,Cat 包含了 Animal 中的所有属性,除此之外,它还有一个额外的方法 run。TypeScript 并不关心 CatAnimal 之间定义时是什么关系,而只会看它们最终的结构有什么关系——所以它与 Cat extends Animal 是等价的:

1
2
3
4
5
6
interface Animal {
name: string;
}
interface Cat extends Animal {
run(): void;
}

我们把它换成 TypeScript 中更专业的说法,即:Animal 兼容 Cat

Animal 兼容 Cat 时,它们就可以互相进行类型断言了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}

function testAnimal(animal: Animal) {
return (animal as Cat);
}
function testCat(cat: Cat) {
return (cat as Animal);
}

父类可以断言为子类,子类也可以断言为父类。

双重断言

既然任何类型都可以被断言为any,any也可以被断言为任何类型,那么我们可以使用双重断言 as any as Foo 来将任何一个类型断言为任何另一个类型。

1
2
3
4
5
6
7
8
9
10
interface Cat {
run(): void;
}
interface Fish {
swim(): void;
}

function testCat(cat: Cat) {
return (cat as any as Fish);
}

若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。

除非迫不得已,千万别用双重断言。

类型断言与类型转换

类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除:

1
2
3
4
5
6
function toBoolean(something: any): boolean {
return something as boolean;
}

toBoolean(1);
// 返回值为 1

在上面的例子中,将 something 断言为 boolean 虽然可以通过编译,但是并没有什么用,代码在编译后会变成:

1
2
3
4
5
6
function toBoolean(something) {
return something;
}

toBoolean(1);
// 返回值为 1

所以类型断言不是类型转换,它不会真的影响到变量的类型。若要进行类型转换,需要直接调用类型转换的方法。

类型断言与类型声明

在这个例子中:

1
2
3
4
5
6
7
8
9
10
11
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();

我们使用 as Catany 类型断言为了 Cat 类型。

但实际上还有其他方式可以解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
function getCacheData(key: string): any {
return (window as any).cache[key];
}

interface Cat {
name: string;
run(): void;
}

const tom: Cat = getCacheData('tom');
tom.run();

上面的例子中,我们通过类型声明的方式,将 tom 声明为 Cat,然后再将 any 类型的 getCacheData('tom') 赋值给 Cat 类型的 tom

这和类型断言是非常相似的,而且产生的结果也几乎是一样的——tom 在接下来的代码中都变成了 Cat 类型。

它们的区别,可以通过这个例子来理解:

1
2
3
4
5
6
7
8
9
10
11
12
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}

const animal: Animal = {
name: 'tom'
};
let tom = animal as Cat;

在上面的例子中,由于 Animal 兼容 Cat,故可以将 animal 断言为 Cat 赋值给 tom

但是若直接声明 tomCat 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}

const animal: Animal = {
name: 'tom'
};
let tom: Cat = animal;

// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.

则会报错,不允许将 animal 赋值为 Cat 类型的 tom

这是因为,子类实例可以赋值给类型为父类的变量,而父类实例不可以赋值给类型为子类的变量

深入的讲,它们的核心区别就在于:

  • animal 断言为 Cat,只需要满足 Animal 兼容 CatCat 兼容 Animal 即可
  • animal 赋值给 tom,需要满足 Cat 兼容 Animal 才行(即tom的类型是animal的父类),上例并不满足。

而any类型是双重兼容任意类型,所以此时的类型断言与类型声明等价。

类型断言与泛型

通过给 getCacheData 函数添加了一个泛型 <T>,我们可以更加规范的实现对 getCacheData 返回值的约束,这也同时去除掉了代码中的 any,是最优的一个解决方案。

Js中类的用法

属性和方法

使用 class 定义类,使用 constructor 定义构造函数。

通过 new 生成新实例的时候,会自动调用构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
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

类的继承

使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法。

1
2
3
4
5
6
7
8
9
10
11
12
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

存取器

使用 getter 和 setter 可以改变属性的赋值和读取行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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); // Jack

静态方法

使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:

1
2
3
4
5
6
7
8
9
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

ES7 中有一些关于类的提案,TypeScript 也实现了它们,这里做一个简单的介绍。

实例属性

ES6 中实例的属性只能通过构造函数中的 this.xxx 来定义,ES7 提案中可以直接在类里面定义:

1
2
3
4
5
6
7
8
9
10
class Animal {
name = 'Jack';

constructor() {
// ...
}
}

let a = new Animal();
console.log(a.name); // Jack

静态属性

ES7 提案中,可以使用 static 定义一个静态属性:

1
2
3
4
5
6
7
8
9
class Animal {
static num = 42;

constructor() {
// ...
}
}

console.log(Animal.num); // 42

TS中类的用法

public private 和 protected

TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 publicprivateprotected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

当构造函数修饰为 private 时,该类不允许被继承或者实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal {
public name;
private constructor(name) {
this.name = name;
}
}
class Cat extends Animal {
constructor(name) {
super(name);
}
}

let a = new Animal('Jack');

// index.ts(7,19): TS2675: Cannot extend a class 'Animal'. Class constructor is marked as private.
// index.ts(13,9): TS2673: Constructor of class 'Animal' is private and only accessible within the class declaration.

当构造函数修饰为 protected 时,该类只允许被继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
public name;
protected constructor(name) {
this.name = name;
}
}
class Cat extends Animal {
constructor(name) {
super(name);
}
}

let a = new Animal('Jack');

// index.ts(13,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the

修饰符可以当作参数用在构造函数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。

1
2
3
4
5
6
class Animal {
// public name: string;
public constructor(public name) {
// this.name = name;
}
}

readonly

只读属性关键字,只允许出现在属性声明或索引签名或构造函数中。

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
readonly name;
public constructor(name) {
this.name = name;
}
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.

注意如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。

1
2
3
4
5
6
class Animal {
// public readonly name;
public constructor(public readonly name) {
// this.name = name;
}
}

抽象类

abstract 用于定义抽象类和其中的抽象方法。

什么是抽象类?

首先,抽象类是不允许被实例化的:

1
2
3
4
5
6
7
8
9
10
11
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}

let a = new Animal('Jack');

// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.

上面的例子中,我们定义了一个抽象类 Animal,并且定义了一个抽象方法 sayHi。在实例化抽象类的时候报错了。

其次,抽象类中的抽象方法必须被子类实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}

class Cat extends Animal {
public eat() {
console.log(`${this.name} is eating.`);
}
}

let cat = new Cat('Tom');

// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.

上面的例子中,我们定义了一个类 Cat 继承了抽象类 Animal,但是没有实现抽象方法 sayHi,所以编译报错了。

下面是一个正确使用抽象类的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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');

子类继承抽象类,同时需要实现抽象方法。

需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类

类的类型

给类加上 TypeScript 的类型很简单,与接口类似:

1
2
3
4
5
6
7
8
9
10
11
12
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

类与接口

之前学习过,接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述。

这一章主要介绍接口的另一个用途,对类的一部分行为进行抽象。

类实现接口

实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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');
}
}

一个类可以实现多个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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');
}
}

上例中,Car 实现了 AlarmLight 接口,既能报警,也能开关车灯。

接口继承接口

接口与接口之间可以是继承关系:

1
2
3
4
5
6
7
8
interface Alarm {
alert(): void;
}

interface LightableAlarm extends Alarm {
lightOn(): void;
lightOff(): void;
}

这很好理解,LightableAlarm 继承了 Alarm,除了拥有 alert 方法之外,还拥有两个新方法 lightOnlightOff

接口继承类

常见的面向对象语言中,接口是不能继承类的,但是在 TypeScript 中却是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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};

实际上,当我们在声明 class Point 时,除了会创建一个名为 Point 的类之外,同时也创建了一个名为 Point 的类型(实例的类型)。

所以我们既可以将 Point 当做一个类来用(使用 new Point 创建它的实例):

1
2
3
4
5
6
7
8
9
10
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}

const p = new Point(1, 2);

也可以将 Point 当做一个类型来用(使用 : Point 表示参数的类型):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}

function printPoint(p: Point) {
console.log(p.x, p.y);
}

printPoint(new Point(1, 2));

这个例子实际上可以等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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));

上例中我们新声明的 PointInstanceType 类型,与声明 class Point 时创建的 Point 类型是等价的。

值得注意的是,PointInstanceType 相比于 Point,缺少了 constructor 方法,这是因为声明 Point 类时创建的 Point 类型是不包含构造函数的。另外,除了构造函数是不包含的,静态属性或静态方法也是不包含的(实例的类型当然不应该包括构造函数、静态属性或静态方法)。

也就是说,声明 Point 类时创建的 Point 类型只包含其中的实例属性和实例方法

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:

1
2
3
4
5
6
7
8
9
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']

上例中,我们使用了之前提到过的数组泛型来定义返回值的类型。

这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:

Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。

这时候,泛型就派上用场了:

1
2
3
4
5
6
7
8
9
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']

上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。

接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来:

1
2
3
4
5
6
7
8
9
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(3, 'x'); // ['x', 'x', 'x']

多个类型参数

定义泛型的时候,可以一次定义多个类型参数:

1
2
3
4
5
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

上例中,我们定义了一个 swap 函数,用来交换输入的元组。

泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

1
2
3
4
5
6
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
}

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

1
2
3
4
5
6
7
8
interface Lengthwise {
length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}

上例中,我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。

此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错。

多个类型参数之间也可以互相约束:

1
2
3
4
5
6
7
8
9
10
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 });

上例中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。

泛型接口

之前学习过,可以使用接口的方式来定义一个函数需要符合的形状:

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}

当然也可以使用含有泛型的接口来定义函数的形状:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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']

进一步,我们可以把泛型参数提前到接口名上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface CreateArrayFunc<T> {
(length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<any>;
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']

注意,此时在使用泛型接口的时候,需要定义泛型的类型。

泛型类

与泛型接口类似,泛型也可以用于类的类型定义中:

1
2
3
4
5
6
7
8
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; };

泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

1
2
3
4
5
6
7
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;
}

声明合并

如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型:

函数的合并

之前学习过,我们可以使用重载定义多个函数类型:

1
2
3
4
5
6
7
8
9
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('');
}
}

接口的合并

接口中的属性在合并时会简单的合并到一个接口中:

1
2
3
4
5
6
interface Alarm {
price: number;
}
interface Alarm {
weight: number;
}

相当于:

1
2
3
4
interface Alarm {
price: number;
weight: number;
}

注意,合并的属性的类型必须是唯一的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Alarm {
price: number;
}
interface Alarm {
price: number; // 虽然重复了,但是类型都是 `number`,所以不会报错
weight: number;
}
interface Alarm {
price: number;
}
interface Alarm {
price: string; // 类型不一致,会报错
weight: number;
}

// index.ts(5,3): error TS2403: Subsequent variable declarations must have the same type. Variable 'price' must be of type 'number', but here has type 'string'.

接口中方法的合并,与函数的合并一样:

1
2
3
4
5
6
7
8
interface Alarm {
price: number;
alert(s: string): string;
}
interface Alarm {
weight: number;
alert(s: string, n: number): string;
}

相当于:

1
2
3
4
5
6
interface Alarm {
price: number;
weight: number;
alert(s: string): string;
alert(s: string, n: number): string;
}

类的合并

类的合并与接口的合并规则一致。

文档参考

TypeScript入门教程:学TypeScript时跟着这个教程学到了最后,自己又再次整理了一遍。感谢原作者。

TypeScript官方手册中文版:感谢 @zhongsp大佬对官方手册的翻译。