从0开始的TypeScriptの四:接口Interfaces
接口 Interfaces (上篇)
在 typescript
中,有一项比较重要的核心知识,那就是接口Interfaces
接口是什么
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对对象的形状(Shape)进行描述。在TS中,接口的作用是为这些类型命名和为你的代码或第三方代码定义契约。
并且接口只对typescript
编译时有影响,对运行时影响为0
简单的接口
下面的例子中,定义了一个名为Cat
的接口,定义了两个个变量 tom
和 jerry
, 类型是Cat
。
这样给tom和Jerry
赋值时就约束了tom
和 jerry
的形状必须和接口Cat
一致,也就是定义的变量不能比接口多出属性或者少出属性。
但是可以运行改变赋值时属性的顺序,因为类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以
接口一般首字母大写
interface Cat {
name: string;
age: number;
}
let tom: Cat = {
name: 'Tom',
age: 10,
}
let jerry: Cat = {
age: 10,
name: 'jerry',
}
在给一个方法的参数定义接口时,方法调用时有个地方需要注意:
interface Pear {
name: string;
size: number
};
function fun2( arg1: Pear ): void {
console.log(arg1);
}
此方法调用传参时,如果直接传入对象,对象需要和接口一一对应,如果有多余属性会出现报错,当然有两种方法可以避免:
- 一种就是使用
类型断言
将传入对象断言为所需要的类型。 - 一种是先把传参的对象赋值给一个变量再传入方法内,只要需要的属性存在,其他多余的属性也不会受影响而报错。
PS:这个把传入对象赋值给变量后多余属性传入没有报错我也很奇怪,没找到原因,如果有好心人可以在评论区讲解一下,多谢多谢
fun2({ name: 'Pear', size: 10})
fun2({ name: 'Pear', size: 10, age: 5}) // error: Object literal may only specify known properties, and 'age' does not exist in type 'Pear'.
fun2({ name: 'Pear', size: 10, age: 5} as Pear) // 使用类型断言
var argobj = { name: 'Pear', size: 10, age: 5};
fun2(argobj)
可选属性
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。
可选属性在应用option bags选择包模式
时很常用,即给函数传入的参数对象中只有部分属性赋值了。
例子:
interface Options {
check1: boolean,
check2?: boolean, // check2和check3都是可选属性
check3?: boolean
}
// 定义赋值时可不附值可选属性
let c1: Options = {
check1: false
}
let c2: Options = {
check1: true,
check3: false
}
可以通过只读属性配置一个ajax参数接口,这样options和dataType就是可传可不传:
interface Configajax {
url: string,
options?: (string|number|null|undefined|object)[],
method: string,
dataType?: any
}
ajax的方法:
// 利用promise封装一个ajax,可以使用then返回数据
// 如果使用promise等es6,在编译时可以指定运行环境 tsc --target es6 tsc03.ts
function getAjaxNew(config: Configajax) {
let promise = new Promise((resolve, reject) => {
const req = new XMLHttpRequest();
let readystatechange = ()=>{
if(req.readyState === 4) { //判断响应状态是否成功
let responseHeaders = req.getAllResponseHeaders(); //获取响应头信息
let data = req.responseText; //获取响应数据
// 数据处理
resolve(req.response);
}
}
req.onreadystatechange = readystatechange;
if (config.dataType)
req.responseType = config.dataType;
if (config.method == 'GET') {
if (config.options)
req.open(config.method, config.url + '?' + config.options.join('&'), true); // true代表异步
else
req.open(config.method, config.url , true); // true代表异步
req.setRequestHeader('X-Requested-with','XMLHttpRequest');//设置请求头信息
req.send();//发送请求
} else {
req.open(config.method, config.url, true); // true代表异步
req.setRequestHeader('Content-Type','application/json;charset=UTF-8');//设置请求头信息,请求头需要在open之后
if (config.options) {
let data = JSON.stringify(config.options);
req.send(data);//发送请求
} else{
req.send();//发送请求
}
}
})
return promise;
}
传参调用方法:
let doptions = [{ user:'kong', newUrl:'www.baidu.com' },{ user:'z', newUrl:'www.zhihu.com' },{ user:'k', newUrl:'www.kantu.com' }];
ajax({
url:"http://localhost:3001/typescript",
method:'GET',
options: doptions
}).then((response)=>{
console.log(response)
});
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值, 可以在属性名前用readonly来指定只读属性。
此属性与es6中的const常量块级作用域有些类似,不允许后续继续改变
interface Changes {
readonly id: number,
type: number|string
}
let changenum: Changes = {
id: 10,
type: 765
}
changenum.id = 5; // error: Cannot assign to 'id' because it is a read-only property.
changenum.type = '好'
注意:只读的约束存在于第一次给
对象
赋值的时候,而不是第一次给只读属性
赋值
最简单判断该用readonly
还是const
的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用const
,若做为属性则使用readonly
interface Changes {
readonly id?: number,
type: number|string
}
let changenum: Changes = {
type: 765
}
changenum.id = 5; // 此时虽然是对id属性第一次赋值,但是changenum对象已经赋值过了,所以还是会报错
在TypeScript
中有只读数组readonlyArray
的定义,readonlyArray
的用法与Array
类似,但是赋值后是不可变的
let changearr: ReadonlyArray< number > = [1, 2, 3, 4, 5]
changearr[4] = 0; // error: Index signature in type 'readonly number[]' only permits reading.
let arr5:number[] = []
arr5.push(changearr) // error: Argument of type 'readonly number[]' is not assignable to parameter of type 'number'.
除了readonlyArray
外,在TypeScript
中还有只读MapReadonlyMap
的存在
let oldMap = new Map();
oldMap.set("1", { name: '123' })
let changeMap: ReadonlyMap<string, object> = new Map(oldMap); // 深拷贝
changeMap.set("2", { name: '2222' }) // error: Property 'set' does not exist on type 'ReadonlyMap<string, object>'.
任意属性
有时候我们希望一个接口允许有任意的属性
任意属性有两种定义方式:
- 属性签名是string字符串的,
[propName: string]: any;
- 属性签名是number数值类型的,
[propIndex: number]: any;
注意:在赋值时,这两种方式还是略有不同的
interface anyinter {
[propName: string]: any;
}
// 这里的属性名使用了number类型,但是也没有出错,这是因为数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 相当于moreinter[0] 等价于 moreinter['0']
let moreinter: anyinter = {
1: false,
"two": 123,
"three": '888',
0: 1
}
interface anyinter2 {
[propIndex: number]: any;
}
let moreinter2: anyinter = [
false,
123,
'888',
]
一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
如果同时使用两种定义方式:
interface anyinter3 {
[propName: string]: boolean;
[propIndex: number]: false; // 必须是boolean的子集
// [propIndex: number]: number; // error: Numeric index type 'number' is not assignable to string index type 'boolean'
}
let moreinter3: anyinter3 = {
'1': false,
'2': true // error: Type 'true' is not assignable to type 'false' 不能将类型“true”分配给类型“false”
}
接口 Interfaces (下篇)
函数与接口
接口能够描述 JavaScript
中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型
。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
下面的例子: 把接口定义给函数md0,这样函数md0的参数和返回值就需要和接口定义的类型相同了
interface encrypt {
(key: string, value: string): string;
}
let md0:encrypt = function(key: string, value: string):string {
return key + value;
}
类继承接口
TypeScript
也能够用接口来明确的强制一个类去符合某种契约,类中的抽象类概念有些相似,都是对类进行约束
类如果是继承父类,使用extends
。 但是如果是接受接口的约束,就使用implements
关键字 (让我想起了学Java的时候)
下面是一个简单的类继承接口的例子:
interface TennisRules {
server: string;
catchball(score: number): void;
}
class Tennis implements TennisRules {
server: string;
constructor(server: string) {
this.server = server;
}
catchball(score: number) {
console.log(`由发球者${this.server}发出的球,获得的接球分数为:${score}分`);
}
}
let ten1 = new Tennis('张三');
ten1.catchball(9); // 由发球者张三发出的球,获得的接球分数为:9分
这样一看好像声明一个像下面的一样的类,然后继承也同样可以实现:
class TennisRules2 {
server: string;
constructor(server: string) {
this.server = server;
}
catchball(score: number) {}
}
实际上这两者虽然相似,但是还是有区别的。
类与接口区别:
接口 | 类 |
---|---|
接口中只声明成员方法,不做实现 | 类声明并实现方法(抽象方法可不实现) |
可以继承多个接口,用逗号分隔 | 只能继承一个类,另外implements 也可以继承类 |
接口不能实例 | 类可以实例化 |
接口继承类
当然不仅类可以继承接口,反过来,接口也可以继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。
定义一个Money类:
class Money {
facevalue: number;
constructor(facevalue: number) {
this.facevalue = facevalue;
}
buy():void {
console.log('买东西');
}
}
接口继承类,并使用。 其中的属性可以使用,但是buy方法不行
// 接口继承类
interface dollar extends Money {
size: number
}
let dollar_one = {size: 10, facevalue: 100} as dollar;
console.log(dollar_one.facevalue); // 100
console.log(dollar_one.size); // 10
dollar_one.buy(); // 虽然编译无报错,但是无法使用 dollar_one.buy is not a function
接口继承接口
和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
例子:
interface ShapeColor {
color: string;
}
interface ShapeHeight {
height: number
}
// 一个接口可以继承多个接口,创建出多个接口的合成接口
interface Square extends ShapeColor, ShapeHeight {
width: number;
}
let square = {} as Square;
square.color = '#f00';
square.width = 30;
square.height = 50;
console.log(square);
- 点赞
- 收藏
- 关注作者
评论(0)