这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战
前面学习了基础类型、数组元组、函数类型和类类型,可以用于描述业务场景中一般的数据类型。对于更复杂的数据结构,还得看今天的学习内容 —— 接口类型(interface)。
Interface 接口类型
接口类型的定义和使用
interface People {
name:string;
age:number;
language:string[];
}
function newPerson(personalInfo:People){
console.log(`My name is ${personalInfo.name}, ${personalInfo.age} years old. I can speak ${personalInfo.language.join(', ')}`)
}
let lofi = newPerson({
name:'lofi',
age:27,
language:['Chinese','English','Cantonese']
})
上述例子定义了一个 People 接口,描述了该 People 中必须包含 name, age, language 三个属性。在Person 方法中接受的参数定义为 People 类型。
可缺省属性和只读属性
我们也可以使用 ?:
把某些属性定义为可缺省的;使用 readonly
关键字定义为只读属性。
interface People {
readonly name:string;
age?:number;
language:string[];
}
function newPerson(personalInfo:People){
//personalInfo.name = personalInfo.name + '***' //error: 无法分配到 “name”,因为它是常数或只读属性。
console.log(`My name is ${personalInfo.name}, ${personalInfo.age} years old. I can speak ${personalInfo.language.join(',')}`)
}
newPerson({
name:'lofi',
language:[]
}) //ok, age 可缺省
对于只读属性,如果我们试图去修改,TS 会给出报错。
ReadonlyArray
TS 中还有 ReadonlyArray<T>
类型,它与Array<T>
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。
let arr: number[] = [1,2,3,4]
let roArr: ReadonlyArray<number> = a;
roArr[0] = 100 //error
a = roArr //error!把 ReadonlyArray 赋值到一个普通数组也是不可以的
a = ro as number[]; //ok ,可以使用类型断言重写
readonly 和 const
readonly 定义一个不被改变的属性;const 定义一个不被改变的变量(常量)
额外的属性检查
在上述例子中,People 定义了期望得到的 name, age 和 language 属性,如果给 People 传一个额外的参数会怎么样呢?
interface People {
readonly name:string;
age?:number;
language:string[];
}
function newPerson(personalInfo:People){
console.log(`My name is ${personalInfo.name}, ${personalInfo.age} years old. I can speak ${personalInfo.language.join(', ')}`)
}
let lofi = newPerson({
name:'lofi',
age:27,
language:['Chinese','English','Cantonese'],
skills:[] //error: 只能指定已知属性,skills 不在类型 People 中
})
该有的都有,多了也不行吗?是的,TypeScript对对象字面量进行额外的属性检查,如果一个对象字面量存在任何“目标类型”不包含的属性时,你会看到下面这个错误提示:
当然,想绕开这个错误也是可以的 ——
类型断言
类型断言允许我们覆盖 TS 的推断:使用 as
关键字
let lofi = newPerson({
name:'lofi',
age:27,
language:['Chinese','English','Cantonese'],
skills:[] //error:
} as People) //像这样,明确的告知 ts,这个就是 People 类型,它就闭嘴了。
但类型断言是不推荐使用的,因为这样的话,如果代码中真的有不匹配的类型也不会被发现了。 对于想要给已知类型添加额外属性的场景,还有另一种更合适的解决方案 —— 索引签名
添加一个字符串索引签名
如果你确定这个对象可能还有一些额外的属性,也可以给它添加一个字符串索引签名,使之能接收任意数量的其他属性。
interface People {
readonly name:string;
age?:number;
language:string[];
[propName: string]: any; //添加一个索引签名
}
function newPerson(personalInfo:People){
console.log(`My name is ${personalInfo.name}, ${personalInfo.age} years old. I can speak ${personalInfo.language.join(', ')}`)
}
let lofi = newPerson({
name:'lofi',
age:27,
language:['Chinese','English','Cantonese'],
skills:[] //这样就不报错啦。
})
还有一个“骚操作”
除了上面两种方式,借助另一个变量,也能绕过额外的属性检查,像这样:
let info = {
name:'lofi',
age:27,
language:['Chinese','English','Cantonese'],
skills:[] //这样就不报错啦。
}
let lofi = newPerson(info)
这种方式只会绕过额外的属性检查,对于不可缺省的属性,仍然会进行检查。
定义函数类型
接口中同样可以定义函数类型,但仅仅定义函数的参数及其返回值类型,不包含函数的具体实现。如下所示:
//定义了一个 Study 接口类型
interface Study {
(skills: string[]): void //一个函数类型的匿名成员。函数参数为 string[] 类型,返回值为 void 类型
}
let StudyFn: Study = skills => console.log(`${skills.join(',')}`);
实际上,我们还可以使用内联类型或类型别名配合箭头函数语法来定义函数类型。如,把上述例子改写为:
type Study = ( skills: string[] ) => void;
继承与实现
继承 extends (接口继承接口)
在 TS 中,接口类型可以通过 extends 关键字实现接口的继承。
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
//可以继承多个接口
interface Square extends Shape, PenStroke {
sideLength: number; //定义新属性
}
let square:Square = {
color:"blue",
penWidth: 10,
sideLength:10
};
实现 implements (类实现接口)
implements 关键字用于“类实现接口”,或者说,用接口来约束类。
这里 提供了一个很通俗的例子:
门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它,如下 Alarm。
interface Alarm {
alert(): void;
}
interface Light {
lightOn(): void;
lightOff(): void;
}
class Door { }
class SecurityDoor extends Door implements Alarm {
alert() {
console.log('SecurityDoor alert');
}
}
//类可以实现多个接口
class Car implements Alarm, Light {
alert() {
console.log('Car alert');
}
}