TypeScript - 接口
# 1. 接口(Interface)
TypeScript 的一个核心原则是通过检查值的结构来确定类型,这种类型检查被称为「鸭式辨型法」或「结构性子类型化」。接口在 TypeScript 中用于为这些结构命名,并为你的代码或第三方代码定义契约。
# 接口的作用
- 定义结构:接口用于定义对象的结构,确保对象符合特定的类型。
- 抽象方法:接口中的所有方法和属性都是没有实现的,即接口中的所有方法都是抽象方法。
- 实现接口:类可以实现接口,必须提供接口中定义的所有属性和方法。
- 限制对象:接口可以限制对象的形状,使对象必须包含接口中定义的所有属性。
# 接口的定义
接口的定义类似于一个模板,描述了对象应当具有的属性和方法。
示例:
// 定义一个接口 Demo
interface Demo {
name: string; // 必须有一个 name 属性,类型为 string
age: number; // 必须有一个 age 属性,类型为 number
checked: boolean; // 必须有一个 checked 属性,类型为 boolean
}
// 定义一个符合接口结构的对象
let demo: Demo = {
name: "kele", // name 属性为 string 类型
age: 18, // age 属性为 number 类型
checked: true // checked 属性为 boolean 类型
}
2
3
4
5
6
7
8
9
10
11
12
13
- 接口命名规范:接口名称通常以大写字母开头,属性和方法之间可以用空格、逗号或分号分隔。
- 类型检查:如果属性的值不符合接口的类型要求(例如
checked
属性不是boolean
类型),编译器将报错。
# 2. 接口初探
通过一个简单的示例来观察接口是如何工作的:
# 使用对象字面量进行类型检查
在 TypeScript 中,可以直接在函数参数中定义对象的类型结构,这种方式适合简单的类型定义。
示例:
// 定义一个函数,要求参数为具有 label 属性的对象
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
// 定义一个符合类型要求的对象
let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj); // 输出:Size 10 Object
2
3
4
5
6
7
8
- 类型检查器:TypeScript 类型检查器会检查
printLabel
函数调用的参数对象是否具有label
属性,并且类型为string
。 - 多余属性:传入的对象可以包含比类型要求更多的属性,编译器只检查那些必需的属性是否存在,以及它们的类型是否匹配。
# 使用接口进行类型定义
可以将对象的结构定义为接口,以提高代码的可读性和可维护性。
示例:
// 定义一个接口,描述具有 label 属性的对象
interface LabelledValue {
label: string; // label 属性必须是 string 类型
}
// 使用接口作为函数参数的类型
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
// 定义一个符合接口要求的对象
let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj); // 输出:Size 10 Object
2
3
4
5
6
7
8
9
10
11
12
13
- 接口 LabelledValue:用于描述必须包含一个
label
属性且类型为string
的对象。 - 结构匹配:在 TypeScript 中,接口检查的是对象的结构而不是对象是否显式实现了接口,只要对象具有接口定义的所有属性即可通过检查。
注意事项
- 属性顺序:类型检查器不会检查属性的顺序,只要属性名称和类型正确即可。
- 接口兼容性:接口的检查基于值的外形(结构),只要符合结构定义即可被接受。
接口是 TypeScript 强大的类型系统的一部分,它允许开发者定义更明确的类型约束,提高代码的可靠性和可维护性。通过接口,开发者可以在编写和维护大型项目时减少错误,并确保模块之间的良好协作。
# 3. 可选属性
在 TypeScript 中,接口中的属性不一定都是必需的。对于某些情况,一些属性可能仅在特定条件下存在,或者在某些对象中可能根本不存在。为此,TypeScript 提供了可选属性,可以使用 ?
符号标记。
# 可选属性的定义
在接口中定义可选属性时,只需在属性名后添加一个 ?
符号。
示例:
// 定义一个接口 SquareConfig,其中 color 和 width 为可选属性
interface SquareConfig {
color?: string; // color 属性是可选的
width?: number; // width 属性是可选的
}
// 定义一个接口 Square,其中 color 和 area 为必需属性
interface Square {
color: string; // color 属性是必需的
area: number; // area 属性是必需的
}
// 定义一个函数 createSquare,接受一个 SquareConfig 类型的参数,并返回一个 Square 类型的对象
function createSquare(config: SquareConfig): Square {
let newSquare = { color: 'white', area: 100 };
if (config.color) { // 检查 color 属性是否存在
newSquare.color = config.color; // 如果存在,则使用配置的 color
}
if (config.width) { // 检查 width 属性是否存在
newSquare.area = config.width * config.width; // 如果存在,则计算 area
}
return newSquare;
}
// 使用 createSquare 函数创建一个 Square 对象
let mySquare = createSquare({ color: 'black' });
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 可选属性的优点
- 灵活性:可选属性允许你在对象中定义一些非必需的属性,使得对象初始化更加灵活。
- 错误捕获:当试图访问不存在的属性时,TypeScript 会提供错误提示,帮助开发者发现拼写错误或逻辑错误。
示例:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): Square {
let newSquare = { color: 'white', area: 100 };
if (config.clor) { // 错误:属性 'clor' 不存在于类型 'SquareConfig' 中
newSquare.color = config.clor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用场景
- 当一个对象的属性是可选时,可以使用可选属性来修饰这些属性。
- 没有
?
修饰的属性必须在对象初始化时赋值,而可选属性则不需要。
# 4. 只读属性
在 TypeScript 中,有些对象属性只允许在对象创建时赋值,之后不能再被修改。这种属性可以使用 readonly
修饰。
# 只读属性的定义
在属性名前加上 readonly
关键字即可将其标记为只读属性。
示例:
// 定义一个接口 Point,其中 x 和 y 是只读属性
interface Point {
readonly x: number; // x 属性是只读的
readonly y: number; // y 属性是只读的
}
// 创建一个 Point 对象
let p1: Point = {
x: 10,
y: 20
};
// p1.x = 5; // 错误:无法分配到 "x" ,因为它是只读属性
2
3
4
5
6
7
8
9
10
11
12
13
# ReadonlyArray 类型
TypeScript 提供了 ReadonlyArray<T>
类型,与普通数组类似,但所有可变方法都被移除,以确保数组在创建后不可修改。
示例:
let a: number[] = [1, 2, 3, 4]; // 普通数组
let ro: ReadonlyArray<number> = a; // 只读数组
// ro[0] = 12; // 错误:索引签名只允许读取
// ro.push(5); // 错误:'push' 方法在 'readonly number[]' 类型上不存在
// ro.length = 100; // 错误:无法分配到 "length" ,因为它是只读属性
// a = ro; // 错误:类型 "readonly number[]" 不可分配给类型 "number[]"
2
3
4
5
6
7
- 尝试修改
ReadonlyArray
的元素或调用修改方法会导致错误。 - 可以通过类型断言将
ReadonlyArray
转换为普通数组以解除只读限制:
a = ro as number[]; // 类型断言解除只读限制
# readonly vs const
readonly
:用于对象属性,表示属性只可在对象初始化时赋值。const
:用于变量,表示变量的引用不可更改。
选择方法:
- 若要将某个变量声明为不可重新赋值,使用
const
。 - 若要将对象的某个属性声明为不可修改,使用
readonly
。
通过使用可选属性和只读属性,TypeScript 提供了更灵活的类型系统和更严格的代码检查,有助于提高代码的可靠性和可维护性。
# 5. 额外的属性检查
在 TypeScript 中,接口可以用于定义对象的结构。使用接口时,TypeScript 会检查传入的对象是否符合接口的要求。当你将对象字面量直接传递给函数或赋值给变量时,TypeScript 会进行额外的属性检查,以确保对象不会包含不期望的属性。这种检查有助于捕获潜在的错误,例如属性名拼写错误。
# 问题示例
考虑以下例子,其中定义了一个 SquareConfig
接口,并实现了一个 createSquare
函数:
interface SquareConfig {
color?: string; // 可选属性 color
width?: number; // 可选属性 width
}
// 定义 createSquare 函数,返回一个具有 color 和 area 属性的对象
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {
color: 'white', // 默认 color 值
area: 100 // 默认 area 值
}
if (config.color) {
newSquare.color = config.color; // 如果提供了 color,则覆盖默认值
}
if (config.width) {
newSquare.area = config.width * config.width; // 计算 area 值
}
return newSquare;
}
// 传入的对象字面量包含拼写错误的 colour 属性
let mySquare = createSquare({ colour: 'red', width: 100 });
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 额外属性检查
在上面的代码中,传入 createSquare
的参数对象中有一个 colour
属性,该属性在 SquareConfig
中并未定义。这会导致 TypeScript 提示错误,因为对象字面量会经过额外的属性检查:
// 错误:'colour' 属性不存在于类型 'SquareConfig' 中
let mySquare = createSquare({ colour: 'red', width: 100 });
2
# 解决方法
# 1. 类型断言
可以通过类型断言来绕过额外属性检查:
let mySquare = createSquare({ colour: 'red', width: 100 } as SquareConfig);
然而,类型断言是一种欺骗编译器的方法,仅在确切知道额外属性是无害的情况下使用。
# 2. 索引签名
为接口添加索引签名,以允许对象包含任意数量的额外属性:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any; // 允许其他任意属性
}
2
3
4
5
这样定义后,SquareConfig
可以包含任意数量的其他属性,并且这些属性的类型可以是 any
。
# 3. 变量赋值
将对象字面量赋值给一个变量,然后传递该变量:
let squareOptions = { colour: 'red', width: 100 };
let mySquare = createSquare(squareOptions); // 不会触发额外属性检查
2
这种方法将对象字面量赋值给 squareOptions
变量,避免了对象字面量直接传递时的额外属性检查。
# 最佳实践
- 避免随意绕过额外属性检查。
- 如果支持传入不同名称的属性,应修改接口定义以反映这一点。
- 对于复杂对象,可以使用索引签名来支持额外的动态属性。
# 6. 函数类型
接口不仅可以用来描述对象的结构,还可以用来描述函数的类型。通过在接口中定义调用签名,可以对函数的参数和返回值进行类型检查。
# 函数类型的定义
为了定义函数类型,需要在接口中定义一个调用签名,指定参数列表和返回值类型。
示例:
// 定义一个函数类型的接口 SearchFunc
interface SearchFunc {
(source: string, subString: string): boolean; // 调用签名
}
2
3
4
# 函数类型的使用
一旦定义了函数类型的接口,就可以像使用其他接口一样使用它。下面是如何创建一个函数类型的变量,并将一个符合该类型的函数赋值给它。
示例:
// 定义一个变量 mySearch,其类型为 SearchFunc
let mySearch: SearchFunc;
// 为 mySearch 赋值一个符合 SearchFunc 类型的函数
mySearch = function(source: string, subString: string): boolean {
let result = source.search(subString);
return result > -1; // 返回布尔值,表示是否找到子字符串
}
2
3
4
5
6
7
8
# 参数类型推断
对于函数类型的类型检查,函数的参数名不需要与接口中定义的名字相匹配,只需参数类型匹配即可。此外,TypeScript 会根据赋值的上下文自动推断参数类型。
示例:
let mySearch: SearchFunc;
// 使用推断的参数类型
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
2
3
4
5
6
7
在这个示例中,src
和 sub
参数的类型由 SearchFunc
接口推断出来。函数的返回值类型由返回的布尔值自动推断。
注意事项
- 参数名匹配:函数参数名不需要与接口中的名称相同,只需类型匹配即可。
- 类型推断:当函数直接赋值给接口类型的变量时,TypeScript 会自动推断参数和返回值的类型。
- 类型检查:如果函数返回值的类型不匹配接口定义的类型,类型检查器会发出警告。
通过接口定义函数类型,可以确保函数参数和返回值的一致性,提高代码的可读性和维护性。TypeScript 的类型推断机制也可以简化函数类型的定义过程,使代码更加简洁。
# 7. 可索引的类型
在 TypeScript 中,接口不仅可以用于描述对象和函数的类型,还可以用于描述那些能够通过索引访问的类型,例如数组或字典。可索引类型具有一个索引签名,描述了通过索引获取的值的类型。
# 数字索引签名
数字索引签名用于描述可以通过数字索引获取的元素类型。它类似于数组,常用于表示数组类型的数据结构。
示例:
// 定义一个接口 StringArray,其中数字索引返回 string 类型
interface StringArray {
[index: number]: string; // 索引签名,使用数字索引返回 string 类型
}
// 实例化一个 StringArray 类型的变量 myArray
let myArray: StringArray;
myArray = ['Bob', 'Fred']; // 为 myArray 赋值为一个字符串数组
// 使用数字索引访问数组元素
let myStr: string = myArray[0]; // 访问第一个元素,类型为 string
2
3
4
5
6
7
8
9
10
11
在这个例子中,StringArray
接口定义了一个索引签名 [index: number]: string;
,表示使用数字索引时返回值的类型是 string
。
# 字符串索引签名
字符串索引签名用于描述可以通过字符串索引获取的值的类型。常用于表示对象类型的数据结构。
示例:
class Animal {
name: string; // Animal 类包含一个 name 属性
}
class Dog extends Animal {
breed: string; // Dog 类继承自 Animal,并添加了一个 breed 属性
}
// 定义一个接口 NotOkay,其中字符串索引返回 Dog 类型
interface NotOkay {
[x: string]: Dog; // 字符串索引签名,返回 Dog 类型
// [x: number]: Animal; // 错误:数字索引返回值类型必须是字符串索引返回值类型的子类型
}
2
3
4
5
6
7
8
9
10
11
12
13
在 TypeScript 中,当同时使用字符串和数字索引时,数字索引返回值的类型必须是字符串索引返回值类型的子类型。因为在 JavaScript 中,所有的数字索引都会被转换为字符串索引。
# 字符串索引的应用
字符串索引签名常用于定义字典模式,并确保所有属性与索引签名返回值类型匹配。
示例:
// 定义一个接口 NumberDictionary,其中字符串索引返回 number 类型
interface NumberDictionary {
[index: string]: number; // 字符串索引签名,返回 number 类型
length: number; // 合法,length 是 number 类型
// name: string; // 错误,name 类型与索引返回值类型不匹配
}
2
3
4
5
6
在这个例子中,length
属性符合索引签名的类型要求,但 name
属性不符合。
# 只读索引签名
可以将索引签名设置为只读,防止通过索引修改值。
示例:
// 定义一个接口 ReadonlyStringArray,其中索引签名为只读
interface ReadonlyStringArray {
readonly [index: number]: string; // 只读索引签名
}
let myArray: ReadonlyStringArray = ['Alice', 'Bob'];
// myArray[2] = 'Mallory'; // 错误:无法分配到索引为 2 的元素,因为它是只读属性
2
3
4
5
6
7
# 8. 类的类型
# 实现接口
在 TypeScript 中,接口可以用来确保类遵循某种结构或契约,类似于 C# 或 Java 中的接口。
示例:
// 定义一个接口 ClockInterface,包含一个 currentTime 属性
interface ClockInterface {
currentTime: Date; // currentTime 属性类型为 Date
}
// 定义一个类 Clock,实现 ClockInterface 接口
class Clock implements ClockInterface {
currentTime: Date; // 实现接口中的 currentTime 属性
constructor(h: number, m: number) { // 构造函数
this.currentTime = new Date(); // 初始化 currentTime
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 接口方法
接口可以定义方法,并要求实现接口的类提供具体的实现。
示例:
// 定义一个接口 ClockInterface,包含一个 currentTime 属性和一个 setTime 方法
interface ClockInterface {
currentTime: Date; // currentTime 属性类型为 Date
setTime(d: Date): void; // setTime 方法,接受一个 Date 参数
}
// 定义一个类 Clock,实现 ClockInterface 接口
class Clock implements ClockInterface {
currentTime: Date; // 实现接口中的 currentTime 属性
// 实现接口中的 setTime 方法
setTime(d: Date) {
this.currentTime = d; // 设置 currentTime 为传入的日期
}
constructor(h: number, m: number) { // 构造函数
this.currentTime = new Date(); // 初始化 currentTime
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 类静态部分与实例部分的区别
在实现接口时,需要区分类的静态部分和实例部分。接口只会检查类的实例部分,不会检查静态部分。
示例:
// 定义一个接口 ClockConstructor,用于构造函数
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface; // 构造函数签名
}
// 定义一个接口 ClockInterface,用于实例方法
interface ClockInterface {
tick(): void; // 实例方法
}
// 定义一个工厂函数 createClock,用于创建 ClockInterface 实例
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute); // 使用构造函数创建实例
}
// 定义一个类 DigitalClock,实现 ClockInterface 接口
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { } // 构造函数
tick() {
console.log('beep beep'); // 实现 tick 方法
}
}
// 定义一个类 AnalogClock,实现 ClockInterface 接口
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { } // 构造函数
tick() {
console.log('tick tock'); // 实现 tick 方法
}
}
// 使用 createClock 工厂函数创建 DigitalClock 和 AnalogClock 实例
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
在这个示例中,createClock
函数接受一个 ClockConstructor
类型的构造函数,用于创建符合 ClockInterface
的实例。DigitalClock
和 AnalogClock
类都实现了 ClockInterface
接口,并可以通过 createClock
函数创建其实例。
通过接口和类的结合,TypeScript 提供了一种强大的类型约束机制,帮助开发者编写符合契约的类,同时支持良好的面向对象编程实践。
# 9. 继承接口
在 TypeScript 中,接口可以像类一样进行继承。接口继承允许我们将一个接口中的成员复制到另一个接口中,从而实现接口的模块化和重用。这使得接口更加灵活,可以分割成多个可重用的部分。
# 单接口继承
一个接口可以继承另一个接口,从而获得被继承接口的所有属性和方法。
示例:
// 定义一个接口 Shape,包含一个 color 属性
interface Shape {
color: string;
}
// 定义一个接口 Square,继承自 Shape,并添加了 sideLength 属性
interface Square extends Shape {
sideLength: number;
}
// 使用断言方式实例化 Square 接口
let square = {} as Square;
square.color = 'blue'; // 为继承的 color 属性赋值
square.sideLength = 10; // 为 sideLength 属性赋值
2
3
4
5
6
7
8
9
10
11
12
13
14
在这个示例中,Square
接口继承了 Shape
接口,因此 Square
既包含 Shape
的 color
属性,也包含自己的 sideLength
属性。
# 多接口继承
一个接口可以继承多个接口,创建出一个合成接口,包含所有被继承接口的属性和方法。
示例:
// 定义一个接口 Shape,包含一个 color 属性
interface Shape {
color: string;
}
// 定义一个接口 PenStroke,包含一个 penWidth 属性
interface PenStroke {
penWidth: number;
}
// 定义一个接口 Square,继承自 Shape 和 PenStroke
interface Square extends Shape, PenStroke {
sideLength: number;
}
// 使用断言方式实例化 Square 接口
let square = {} as Square;
square.color = 'blue'; // 为继承的 color 属性赋值
square.sideLength = 10; // 为 sideLength 属性赋值
square.penWidth = 5.0; // 为继承的 penWidth 属性赋值
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在这个示例中,Square
接口继承了 Shape
和 PenStroke
接口,因此它包含了 color
、penWidth
和 sideLength
属性。
# 10. 混合类型
JavaScript 具有动态灵活的特性,TypeScript 接口可以用来描述复杂的类型,包括那些同时具备多种行为的对象。例如,一个对象可以既是一个函数,又有额外的属性。
# 定义混合类型接口
混合类型接口可以同时描述函数和对象的行为。
示例:
// 定义一个接口 Counter,既可以作为函数调用,又包含 interval 属性和 reset 方法
interface Counter {
(start: number): string; // 函数签名
interval: number; // 属性
reset(): void; // 方法
}
// 定义一个函数 getCounter,返回一个符合 Counter 接口的对象
function getCounter(): Counter {
let counter = (function (start: number) {
return 'Counter started at ' + start;
}) as Counter; // 类型断言为 Counter
counter.interval = 123; // 为 interval 属性赋值
counter.reset = function () { // 实现 reset 方法
console.log('Counter reset');
};
return counter;
}
// 获取 Counter 对象,并调用其方法和属性
let c = getCounter();
console.log(c(10)); // 调用函数部分
c.reset(); // 调用 reset 方法
c.interval = 5.0; // 访问 interval 属性
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在这个示例中,Counter
接口描述了一个可以作为函数调用的对象,并且还具有 interval
属性和 reset
方法。通过这种方式,可以灵活地为复杂对象定义类型。
# 11. 接口继承类
接口可以继承类的类型。这种情况下,接口会继承类的成员(包括私有成员和受保护成员),但不包括具体实现。当接口继承了一个包含私有成员或受保护成员的类时,只有这个类或其子类能够实现该接口。
# 接口继承类的示例
示例:
// 定义一个类 Control,包含一个私有成员 state
class Control {
private state: any; // 私有成员 state
}
// 定义一个接口 SelectableControl,继承自 Control,包含一个 select 方法
interface SelectableControl extends Control {
select(): void; // 方法签名
}
// 定义一个类 Button,继承自 Control,并实现 SelectableControl 接口
class Button extends Control implements SelectableControl {
select() { // 实现 select 方法
console.log('Button selected');
}
}
// 定义一个类 TextBox,继承自 Control,并实现 SelectableControl 接口
class TextBox extends Control implements SelectableControl {
select() { // 实现 select 方法
console.log('TextBox selected');
}
}
// 错误:类 'ImageC' 中缺少 'state' 属性
// 定义一个类 ImageC,试图实现 SelectableControl 接口,但不继承 Control
class ImageC implements SelectableControl {
select() { // 实现 select 方法
console.log('ImageC selected');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 关键点
- 私有和受保护成员:当接口继承了一个类时,会继承该类的私有和受保护成员。这意味着只有该类及其子类能够实现该接口。
- 强类型约束:通过继承类,接口可以实现对实现类的更强的类型约束。
在这个示例中,SelectableControl
接口继承了 Control
类。因此,只有 Control
的子类才能实现 SelectableControl
接口。Button
和 TextBox
类可以正常实现接口,因为它们继承自 Control
,而 ImageC
类不能实现接口,因为它没有继承 Control
。
接口继承类是 TypeScript 的一个强大特性,它允许开发者在类与接口之间建立更复杂和灵活的关系,增强类型系统的表达能力。