大家好,我是半虹,这篇文章来讲 TypeScript 中的接口
接口主要用于描述对象的结构,定义对象应该具有的属性和方法
但是本身不会提供对应的实现,可以理解成它是类型检查的工具
1、定义与使用
我们可以使用 interface
关键字定义接口,关键字后是接口的名称和内容
内容使用类似对象字面量的语法,对象字面量中可以声明对象的属性和方法
(1)声明对象属性
对象字面量中可以声明属性,属性名后使用冒号指定属性类型
如果字面量中存在多个属性,那么可以使用逗号或分号来隔开
// 定义
interface Test {
// 声明属性及类型
x: number; // 属性名是 x,对应的属性类型是 number
y: string; // 属性名是 y,对应的属性类型是 string
}
// 使用
// 指定变量类型为上述接口,因此该变量要满足约束,赋值时对象的属性必须:不多、不少、不错
let test:Test = {
x: 123,
y: '0',
}
test.x;
test.y;
属性中会有一些特殊的属性,对应真实对象类型有不同的作用
- 可选属性:可选属性在实际赋值时可以忽略;只需在属性名后加
?
- 只读属性:只读属性在初始赋值后无法修改;只需在属性名前加
readonly
- 索引签名:定义一组通用的对象属性;语法为
[name: T]: U
// 定义
interface Test {
x : number; // 普通属性
y?: number; // 可选属性,可以理解成:y: number|undefiend;
readonly z: number; // 只读属性
[property: string]: number|undefined; // 索引签名
// 索引签名语法为:`[name: T]: U`,要求类型为 T 的属性名,对应的属性值类型为 U,其中 name 可任意指定
// 对应约束具体为:
// 1. 接口中的所有属性都要满足索引签名
// 2. 所有满足索引签名的属性都允许存在
}
// 使用
let test:Test = {
x: 123, // 普通属性
// 可选属性在实际赋值时可以忽略(赋值时对象的属性可少)
z: 345, // 只读属性在初始赋值后无法修改
a: 456, // 所有满足索引签名的属性都允许(赋值时对象的属性可多)
}
test.x = 111; // 编译正常,普通属性可以修改
test.z = 333; // 编译错误,只读属性无法修改
(2)声明对象方法
此外字面量中还能声明方法,声明方法的方式可具体分为两种
- 第一种类似于函数声明,会在函数字面量中指定参数以及返回类型
- 第二种类似于函数表达式,用函数类型签名指定参数以及返回类型
函数表达式的类型签名分为两种,分别是简写版和完整版
-
简写版的语法类似箭头函数,且只可写一条声明
在该声明中,箭头前是函数参数及其类型,箭头后是函数返回类型
-
完整版的语法类似对象形式,且可以写多条声明,即表示函数重载
每条声明中,冒号前是函数参数及其类型,冒号后是函数返回类型
// 定义
interface Test {
// 声明方法及类型
f0(a:number, b:number):number; // 函数声明
f1: (a:number, b:number) => number; // 函数表达式 + 简写版类型签名
f2: { // 函数表达式 + 完整版类型签名(可以理解成接口的嵌套)
(a:number, b:number): number;
}
}
// 使用
// 方法赋值也有三种写法,但这些写法与声明写法无关,可以自由选择,只需满足对于参数和返回类型的约束即可
let test:Test = {
f0(a:number, b:number):number { return a + b; }, // 函数声明
f1: function(a:number, b:number):number { return a + b; }, // 函数表达式 - 普通函数
f2: (a:number, b:number):number => { return a + b; }, // 函数表达式 - 箭头函数(注意 this 的指向)
}
test.f0(1, 2);
test.f1(2, 3);
test.f2(3, 4);
(3)声明普通函数
接口中除了能声明对象的属性和方法外,还能声明独立的函数
类似于对象属性,可以用冒号隔开函数的参数列表和返回类型
// 定义
interface Test {
// 声明普通函数
(a:number, b:number): number;
}
// 注意,普通函数和对象方法的区别:
// 声明是普通函数,则意味着实现该接口的变量是函数,要求函数本身要满足该签名
// 声明是对象方法,则意味着实现该接口的变量是对象,要求对象方法要满足该签名
// 使用
let test:Test = function(a, b) { return a + b; }
test(1, 2);
接口定义中,允许存在多个函数声明,此时表示的是函数重载
但是赋值时,要求一次实现所有声明,参数以及返回类型需要处理所有可能情况
函数调用时,参数需要满足任一声明,并且具体会按函数声明顺序逐一进行匹配
// 定义
interface Test {
(a:number, b:number): number; // 声明 1:传入两个数字,返回一个数字
(a:number[]): number; // 声明 2:传入数字数组,返回一个数字
}
// 使用
let test:Test = function(
a : number|number[], // 如果是声明 1,那么 a 是 number;如果是声明 2,那么 a 是 number[]
b?: number // 如果是声明 1,那么 b 是 number;如果是声明 2,那么 b 为 空,所以该参数为可选
):number { // 无论是声明 1,还是声明 2,返回类型都是 number
if (typeof a === 'number' && typeof b === 'number') { // 处理声明 1
return a + b;
}
if (Array.isArray(a)) { // 处理声明 2
return a.reduce(function(prev, curr) { return prev + curr; });
}
throw new Error('wrong parameters'); // 兜底处理[x]
}
test( 1 ); // 编译错误,单看函数没有问题,x 是 number|number[], y 是可选;但是该传参无法满足任一声明
test( 1, 2 ); // 编译正常,满足声明 1
test([1, 2]); // 编译正常,满足声明 2
需要注意,同时声明普通函数以及对象的属性和方法并不冲突(虽然不经常用)
因为函数是一种特殊的对象,所以函数中也能有属性以及方法
// 定义
interface Test {
// 声明对象属性
x: number;
y: number;
// 声明对象方法
f0(a:number, b:number):number;
// 声明普通函数
(a:number, b:number):number;
}
// 使用
// 此时可以通过一个中间变量过渡赋值
function temp(a:number, b:number):number {
return a + b;
}
temp.x = 1;
temp.y = 2;
temp.f0 = function(a:number, b:number):number {
return a + b;
}
let test:Test = temp;
test(test.x, test.y);
test.f0(test.x, test.y);
(4)声明构造函数
函数中存在一个特殊的函数,称为构造函数,也是类的语法糖
相比于普通函数,构造函数只需在函数声明前加上 new
即可
class Point { // 类
x:number;
y:number;
// 构造函数
constructor(x:number, y:number) {
this.x = x;
this.y = y;
}
}
// 定义
interface Test {
// 声明构造函数
new (a:number, b:number): Point;
}
// 使用
function createPoint(
PointClass: Test, // 构造函数可以代表类的类型
x:number,
y:number,
):Point {
return new PointClass(x, y);
}
let point1:Point = createPoint(Point, 1, 2);
let point2:Point = createPoint(Point, 3, 4);
最后再补充一个接口的特殊用法:接口可以作为类的检查条件
只需在定义类时,在类名称后带:implements
加上接口名称
// 定义
interface Test {
x:number;
y:number;
}
// 使用
// 需要注意的是,此时接口约束的是类中至少需要包含的属性和方法(不同于接口作为类型时:不多、不少、不错)
// 换句话说就是,类中可以包含不存在于接口定义里面的属性和方法
class C implements Test {
x:number;
y:number;
constructor(x:number, y:number) {
this.x = x;
this.y = y;
}
// 不存在于接口中的方法
distanceToOrigin() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
}
2、合并
(1)interface
合并 interface
多个同名接口会自动进行合并,如果这些接口存在同名的属性或方法,表现如下:
-
同名属性的类型不能冲突,否则编译无法通过
-
同名方法会自动进行重载,并且后定义的比前定义的优先级更高
但是这里也有个特殊情况,就是参数有字面量类型的优先级更高
这里还要注意,如果想要重载,那么对象方法只能用函数声明的写法
let temp = function(a:any):any {
return typeof a === 'string' ? Math.round(Math.random()) : -1;
}
// 1. 单个 interface 中的函数重载,优先级是正向声明顺序
interface Test1 {
(a:any):number;
(a:string):0|1;
}
interface Test2 {
(a:string):0|1;
(a:any):number;
}
let test1:Test1 = temp;
let test2:Test2 = temp;
let temp1:0|1 = test1(''); // 编译报错:'' 既能匹配 any,又能匹配 string,但是 any 声明在前,所以匹配 any ,对应返回类型是 number
let temp2:0|1 = test2(''); // 编译正常:'' 既能匹配 string,又能匹配 any,但是 string 声明在前,所以匹配 string,对应返回类型是 0|1
// 2. 多个 interface 中的函数重载,优先级是反向声明顺序
interface Test3 {
x:number;
f(a:string):0|1;
}
interface Test3 {
y:string;
f(a:any):number;
}
// 合并之后等价于:
// interface Test3 {
// x:number;
// y:string;
// // 注意顺序:
// f(a:any):number;
// f(a:string):0|1;
// }
let test3:Test3 = {
x: 123,
y: '0',
f: temp,
}
let temp3:0|1 = test3.f('abcd'); // 编译报错
// 3. 多个 interface 中的函数重载,如果参数有字面量类型,那么优先级会更高
interface Test4 {
x:number;
f(a:'abcd'):0|1;
}
interface Test4 {
y:string;
f(a:any):number;
}
// 合并之后等价于:
// interface Test4 {
// x:number;
// y:string;
// // 注意顺序:
// f(a:'abcd'):0|1;
// f(a:any):number;
// }
let test4:Test4 = {
x: 123,
y: '0',
f: temp,
}
let temp4:0|1 = test4.f('abcd'); // 编译正常
(2)interface
合并 class
除了同名接口能够合并,同名接口和类也可自动合并
其中同名属性不能冲突,同名方法自动重载,这点与同名接口的合并一致
但是合并之后接口和类的表现稍微有些不同:
- 接口合并类后,只关注类中成员的类型,不关注类中成员的实现或值
- 类合并接口后,因定义类时方法有要求必须实现,导致重载有些奇怪
// 类
class Test {
x:number;
f(a:string):0|1 { // 其中方法必须实现,否则编译器会报错
return a.substring(0) === '' ? 0 : 1;
};
}
// 接口
interface Test {
y:string;
f(a:any):number;
}
// 对于接口【还算正常】
// 合并之后,作为类型,需要实现接口和类中定义的所有属性和方法
let test1:Test = {
x: 123,
y: '0',
// 对于其中方法来说,只管类中方法的类型,不管类中方法的实现
// 因此这里重新实现,如果方法是有重载的,那么就要实现重载的
f: function(a:any):any {
return typeof a === 'string' ? Math.round(Math.random()) : -1;
}
}
let temp1:0|1 = test1.f(''); // 编译错误,重载函数依然是有优先级的,此时参数 '' 匹配的是 any【参考上一小节】
// 对于类【就会很奇怪】
// 合并之后,实例对象,其中包含接口和类中定义的所有属性和方法
let test2 = new Test();
test2.x = 123; // 未赋值前是 undefined
test2.y = '0'; // 未赋值前是 undefined
test2.f(1); // 编译正常,运行错误【重要】:TypeError: a.substring is not a function
// 奇怪的点:
// 编译检查时使用的是重载函数的定义,因此编译正常
// 实际运行时使用的是类中函数的实现,因此运行错误
3、继承
(1)interface
继承 interface
一个接口可以继承另外一个接口,前者称为子接口,后者称为父接口
就像类的继承,子接口可以继承父接口的成员,并且新增自己的成员
具体可以使用 extends
关键字来继承父接口,下面举个具体的例子:
interface Father {
x:number;
f(a:number, b:number): number;
}
interface Son extends Father {
y:string;
g(a:string, b:string): string;
}
// 子接口类型必须实现父接口原有成员和子接口新增成员
let son:Son = {
x: 123,
y: '0',
f: function(a:number, b:number):number { return a + b; },
g: function(a:string, b:string):string { return a + b; },
}
需要注意的是,子接口的新增成员可以覆盖父接口的原有成员
如果二者同名,子接口的新增成员需要兼容父接口的原有成员(这里特指类型)
interface Father {
x:number|string;
f():void;
}
interface Son extends Father {
x:number&string; // 其实就是 never
f():boolean;
}
// 类型兼容性是 TypeScript 中的重要概念
// 之后会写一篇文章单独介绍
一个接口可以同时继承多个接口,这些接口之间用逗号来隔开,称为多重继承
多个接口中的同名方法不能重载,其中同名属性和方法的类型都要求必须相同
interface Father1 {
x:number;
f(a:number):number;
}
interface Father2 {
y:any;
f(a:any):any;
}
interface Father3 {
y:string;
g(a:string):string;
}
interface Son extends Father1, Father2 {} // 编译报错
interface Son extends Father1, Father3 { // 编译正常
z:boolean;
h(a:boolean):boolean;
}
let son:Son = {
x: 123,
y: '0',
z: true,
f: function(a:number):number { return a; },
g: function(a:string):string { return a; },
h: function(a:boolean):boolean { return a; },
}
(2)interface
继承 type
此外,接口也可以继承类型别名,此时要求类型别名定义的必须是对象类型
// 类型别名
type Father = {
x:number;
f(a:number, b:number): number;
}
// 接口继承类型别名
interface Son extends Father {
y:string;
g(a:string, b:string): string;
}
let son:Son = {
x: 123,
y: '0',
f: function(a:number, b:number):number { return a + b; },
g: function(a:string, b:string):string { return a + b; },
}
(3)interface
继承 class
最后,接口还可以继承类,此时会继承类中的属性和方法
// 类
class Father {
x:number;
f(a:number, b:number): number { return a + b; }; // 类中方法必须实现,否则编译器会报错
}
// 接口继承类
interface Son extends Father {
y:string;
g(a:string, b:string): string;
}
let son:Son = {
x: 123,
y: '0',
f: function(a:number, b:number):number { return a + b; }, // 只关注类型,不关注实现,这里需要重新定义函数
g: function(a:string, b:string):string { return a + b; },
}
接口和类、接口和类型别名,这三者存在着非常细微的联系和差别,最后来补充一下
(1)接口和类
接口和类都可以作为对象的模版,定义对象应该具备的属性和方法
但是实际上二者有着微妙的区别,具体如下:
-
接口只包含类型代码,不包含值代码,因此编译后就不再存在
而类既包含类型代码,也包含值代码,因此编译后还依然存在
-
接口只声明对象成员的类型,而不具体实现方法
而类既声明对象成员的类型,也会同时实现方法
-
接口可允许多重继承,即一个接口可以继承多个接口
而类只支持单一继承,即一个子类只能继承一个父类
另外,接口和类之间也可以交互,具体如下:
- 接口可以继承类 (
extends
) - 接口可以作为类的检查条件 (
implements
) - 接口和类同名就会自动合并
(2)接口和类型别名
接口和类型别名也能声明对象的模版,定义对象具备的属性和方法
但是实际上二者也是有着微妙的区别,具体如下:
-
接口只能声明对象类型
类型别名则可声明任何类型
-
接口可以使用
extends
关键字拓展接口、类型别名、类类型别名则能使用
&
运算符来合并接口、类型别名 -
同名接口将会自动进行合并
同名类型别名则会导致编译错误
最后稍微总结一下,接口和类型别名其实很像,多数情况下都可以自由选择
关于这一点的说明,可以看一下官网上的介绍
好啦,本文到此结束,感谢您的阅读!
如果你觉得这篇文章有需要修改完善的地方,欢迎在评论区留下你宝贵的意见或者建议
如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)