对象类型是TypeScript的类型系统中最复杂也是最重要的类型,对象类型主要用来描述复杂数据类型:
// 声明一个值为对象字面量
let man = {name: 'joye', age: 30};
// 等价于
let man: {name: string; age: number} = {name: 'joye', age: 30};
在上例第一条语句中,实际上变量 man
会被自动推导为类型 {name: string; age: number}
,它描述了一个对象具有字符串类型的 name
属性和数字类型的 age
属性 。
感谢wenxudong090161 提醒:属性之间可以用分号
;
分隔,也可以用逗号,
分隔,甚至可以只用换行符分隔! 参考Issues #1。为了保持代码的可读性和规范,建议声明的描述中采用;
分隔,以此来区分注解和值
对象类型是匿名的接口类型,对象类型没有名字,接口类型有名字。接口类型相当于为对象类型声明了一个别名:
// 定义接口类型Person
interface Person {
name: string;
age: number;
}
// 声明变量 man 为 Person 接口类型
let man: Person = {name: 'joye', age: 30};
上述语句完全等价于:
let man: {name: string; age: number} = {name: 'joye', age: 30};
本教程后续章节将统一术语:接口代表接口类型,匿名接口代表对象类型
接口的属性是可选的,可选属性类似于函数的可选参数:属性名后添加问号?
即可
interface Person {
name: string;
age: number;
}
// 错误,缺少必选属性 age
// error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
// Property 'age' is missing in type '{ name: string; }'.
let man: Person = {
name: 'joye'
};
将 age
改成可选属性:
interface Person {
name: string;
// 注意此处的问号,age此时为可选属性
age?: number;
}
// 正确
let man: Person = {
name: 'joye'
};
可以通过在属性名前添加 readonly
关键字来指定只读属性,只读属性只能在创建的时候对其赋值,一旦创建完成,就再也不能更改:
interface Person {
// 声明name为只读
readonly name: string;
age: number;
}
// 创建时对只读属性赋值
let man: Person = {
name: 'joye',
age: 30
}
// 错误,只读属性的值不能更改
// error TS2540: Cannot assign to 'name' because it is a constant or a read-only property
man.name = 'mike';
// 正确,非只读属性的值可以更改
man.age = 31;
接口最重要的作用在于描述一个复杂值的外形,通常情况下,接口可以描述:
- 对象字面量
- 函数
- 可索引值
- 类
前面的 Person
接口就是描述对象字面量的例子,此处不再重复举例。
对象字面量的类型匹配非常让人迷惑,请看下面的例子:
interface Person {
name: string;
age: number;
}
// 定义一个对象字面量male
let male = {
name: 'joye',
age: 30,
gender: 'male'
};
// 正确,male包含Person接口的所有属性
let man: Person = male;
在上面的例子中,对象字面量 male
被编译器推导为匿名接口类型,相当于:
// 声明male为匿名接口
let male: {
name: string;
age: number;
gender: string;
};
// 对male赋值
male = {
name: 'joye',
age: 30,
gender: 'male'
};
匿名接口类型包含了 Person
接口的所有属性 name
、age
,编译器认为类型匹配,通过类型检查。然而:
interface Person {
name: string;
age: number;
}
// 直接将对象字面量赋值给接口类型
// 错误,对象字面量直接赋值检查所有属性的兼容性
// 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'.
let man: Person = {
name: 'joye',
age: 30,
gender: 'male'
};
请牢记:对象字面量在直接赋值的时候,编译器会检查字面量类型是否完全匹配,多一个或少一个属性都会报错。
声明一个函数类型的多种方式:
// 描述函数
interface MyFunc {
(name: string, age: number): string;
}
// 声明接口类型
let fn: MyFunc;
// 等价于
let fn: { (name: string, age: number): string; } // 匿名接口
// 等价于
let fn: (name: string, age: number) => string;
// 赋值
fn = function(name: string, age: number): string {
return `${name}, ${age}`;
}
例子,用接口描述一个带静态属性的函数:
// 定义myFunc 函数
function myFunc(){}
// myFunc具有静态属性 `test`
myFunc.test = 'hello world';
// 声明接口类型描述函数 myFunc
interface MyFunc {
// 这条语句描述函数定义
(): void;
// 这条语句描述静态属性 `test`
test: string;
}
// 正确,类型匹配
let newFunc: MyFunc = myFunc;
可索引值一般表示数组类型和对象类型,可以通过键访问某一项的值或属性值。
// 描述一个数组
interface StringArray {
[index: number]: string;
}
// 声明接口类型
let myArray: StringArray;
// 等价于
let myArray: { [index: number]: string; }; // 匿名接口
// 等价于
let myArray: string[];
// 赋值
myArray = ["Bob", "Fred"];
// 描述一个对象
interface MyObject {
[index: string]: string;
}
// 声明接口类型
let myObject: MyObject;
// 赋值
myObject = {
a: '1', b: '2', c: '3'
}
对比前面描述对象字面量的语法,你会发现,这种方式描述对象可以支持无限多的对象属性,上述例子中:
// 省略号代表其他任意属性
myObject = {
a: '1', b: '2', c: '3', d: '4', ...
}
如果一个对象既支持数字索引,也支持字符串索引,这种对象在JavaScript中被称作类数组对象:
// 类数组对象
let obj = {
1: 1,
2: 2,
name: 'joye',
age: 30
}
obj[1] === 1;
obj[2] === 2;
obj['name'] === 'joye';
obj['age'] === 30;
实际上,当采用数字索引方式访问一个值时,JavaScript会将数字索引转换为字符串索引:
obj[1] === 1;
obj[2] === 2;
// 完全等价于
obj["1"] === 1;
obj["2"] === 2;
在TypeScript中,接口类型可以同时描述数字索引类型和字符串索引类型:
// 正确
interface IndexObj {
[x: number]: string;
[x: string]: string;
}
但要注意,由于JavaScript会将数字索引转换为字符串索引,数字索引和字符串索引的值的类型必须相等,或者数字索引的返回值必须是字符串索引返回值类型的子类型:
// 错误,数字索引的值和字符串索引的值不匹配
// error TS2413: Numeric index type 'number' is not assignable to string index type 'string'
interface IndexObj {
[x: number]: number;
[x: string]: string;
}
在 类类型 章节的构造器类型讲解中可以知道,构造器类型代表的就是类本身。用接口来描述一个类:
// 定义一个类
class NewClass {}
// 用接口来描述这个类类型
interface MyClass {
new(): NewClass;
}
// 声明一个变量为描述这个类的接口类型并初始化
let myClass: MyClass = NewClass;
// 等价于
let myClass: typeof NewClass = NewClass;
我们介绍到用接口来描述函数、可索引值、类类型,你会发现还不如直接用类型来声明更直接:
// 声明函数
let myFunc: ()=>{};
// 声明数组
let myArr: string[];
// 声明类
class MyClass {}
let myClass: typeof MyClass;
在实际使用中的确是这样,我们很少直接用接口来声明一个函数或数组。接口更重要的场景在于可以被类实现,从而实现各种复杂的设计模式,在TypeScript中,接口可以被类实现
在面向对象的编程方法学中,接口对于代码可维护性和业务逻辑解耦起着至关重要的作用
// ClockInterface 描述了一个属性和一个方法
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
// 实现接口
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
实现类必须包含接口所声明的全部必选属性,在上面的例子中:Clock
类必须同时包含属性 currentTime
和方法 setTime
:
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
// 错误,缺少属性 currentTime
// error TS2420: Class 'Clock' incorrectly implements interface 'ClockInterface'.
// Property 'currentTime' is missing in type 'Clock'
class Clock implements ClockInterface {
setTime(d: Date) {}
constructor(h: number, m: number) { }
}
接口也可以互相继承,通过继承,子接口将继承父接口的成员:
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
// 正确,color 属性来自父接口
let square: Square = {
color: 'blue',
sideLength: 4
};