TypeScript 中的接口(Interfaces)用于定义对象的结构。它们允许开发者指定一个对象应具有哪些属性以及这些属性的类型。接口有助于确保对象遵循特定的结构,从而在整个应用中提供一致性,并提升代码的可维护性。
一、认识接口
TypeScript 是 JavaScript 的一个静态类型超集,它添加了可选的类型、类、接口以及其他功能,帮助开发者构建健壮且易于维护的应用程序。TypeScript 最强大的特性之一就是接口(interfaces),它允许你定义对象的结构,强制保持一致性,并提升代码的可读性。
(一) 什么是接口
在 TypeScript 中,接口是一种“契约”,用于定义对象的结构。它指定了一个对象必须具备的属性和方法,但不提供具体的实现。接口仅用于类型检查,在编译过程中会被移除。
使用接口有很多好处:
- 定义对象结构:确保对象具有特定的属性和方法。
- 提高代码可读性:清晰地展示对象应包含哪些属性和方法。
- 强制一致性:通过确保对象遵循特定结构来防止错误发生。
以下是一个在 TypeScript 中使用接口的简单示例:
interface Car {
make: string;
model: string;
year: number;
}
const myCar: Car = {
make: "Toyota",
model: "Corolla",
year: 2022
};
console.log(myCar);
Car 接口:定义了 car 对象的结构,其中 make
、model
和 year
是必需的属性。
myCar 对象:一个符合 Car 接口结构的对象,包含指定的属性。
输出:
{ make: 'Toyota', model: 'Corolla', year: 2022 }
(二) 场景示例
1. 类中的接口
// 定义一个 Employee 接口,包含姓名、年龄和职位属性
interface Employee {
name: string;
age: number;
position: string;
}
// Manager 类实现了 Employee 接口,必须包含接口中定义的所有属性
class Manager implements Employee {
name: string;
age: number;
position: string;
// 构造函数用于初始化 name、age 和 position
constructor(name: string, age: number, position: string) {
this.name = name;
this.age = age;
this.position = position;
}
}
// 创建一个 Manager 类的实例
const manager1 = new Manager("John Doe", 35, "Project Manager");
// 输出 manager1 对象
console.log(manager1);
- Employee 接口定义了一个员工的结构,要求包含
name
、age
和position
属性。 - Manager 类实现了 Employee 接口,确保它具有这些必需的属性。
输出:
Manager { name: 'John Doe', age: 35, position: 'Project Manager' }
2. 对象的接口定义
// 定义 Product 接口,包含 id、name 和 price 属性
interface Product {
id: number;
name: string;
price: number;
}
// 创建一个符合 Product 接口的对象 product
const product: Product = {
id: 1,
name: "Laptop",
price: 1200
};
// 输出 product 对象
console.log(product);
- Product 接口规定了一个产品对象必须包含
id
(数字)、name
(字符串)和price
(数字)属性。 - product 对象符合 Product 接口,包含了所有必需的属性。
输出:
{ id: 1, name: 'Laptop', price: 1200 }
3. 带有方法签名的接口
// 定义 Calculator 接口,包含两个方法的签名:add 和 subtract,均接收两个数字参数,返回数字
interface Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
}
// SimpleCalculator 类实现 Calculator 接口,提供 add 和 subtract 方法的具体实现
class SimpleCalculator implements Calculator {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
}
// 创建 SimpleCalculator 实例
const calc = new SimpleCalculator();
// 调用 add 方法,输出结果 8
console.log(calc.add(5, 3));
// 调用 subtract 方法,输出结果 5
console.log(calc.subtract(9, 4));
- Calculator 接口定义了两个方法:add 和 subtract,二者都接受两个数字参数并返回一个数字。
- SimpleCalculator 类实现了 Calculator 接口,为 add 和 subtract 方法提供了具体的逻辑实现。
8
5
(三) 使用接口的最佳实践
- 使用接口定义对象结构:始终使用接口来定义代码中复杂对象或数据模型的结构。
- 使用可选属性:如果某些属性可能不总是存在,可以将它们设为可选,以提高代码的灵活性。
- 接口继承:利用接口继承,从简单接口构建更复杂的结构。
- 使用接口定义函数签名:在定义复杂函数时,接口能帮助明确函数签名,使代码更易维护。
二、interface 和 type 的区别
在 TypeScript 中,interface 和 type 都用于定义对象的结构,但它们在灵活性和使用上有所不同。interface 是可扩展的,主要用于描述对象的形状;而 type 更加多样化,支持联合类型、交叉类型以及更复杂的类型定义。
(一) TypeScript 中的类型
TypeScript 的类型系统描述了语言支持的各种数据类型。它主要分为三大类:任意类型(Any Type)、内置类型(Built-In Type)和用户自定义类型(User-Defined Type)。TypeScript 的类型系统负责在程序运行前确保数据类型的正确性。
在这个示例中,我们定义了两个 TypeScript 类型 —— People 和 MorePeople,并使用交叉类型操作符 &
将它们组合起来。常量 people 同时实现了这两种类型,存储并输出了这个组合后的对象。
// 定义类型 People,包含 name 和 age 属性
type People = {
name: string;
age: number;
};
// 定义类型 MorePeople,包含 email 属性
type MorePeople = {
email: string;
};
// 使用交叉类型将 People 和 MorePeople 组合成 CombinedPeople 类型
type CombinedPeople = People & MorePeople;
// 创建符合 CombinedPeople 类型的对象 people,包含 name、age 和 email 属性
const people: CombinedPeople = {
name: "FelixLu",
age: 20,
email: "FelixLu@gmail.com"
};
// 输出 people 对象
console.log(people);
输出:
{ name: 'FelixLu', age: 20, email: 'FelixLu@gmail.com' }
(二) TypeScript 中的接口
接口(Interface)是 TypeScript 中一种语法上的“契约”,实体必须遵守该契约。接口只能声明属性、方法和事件,而不包含任何具体实现。接口定义了实现类必须遵循的标准结构。
本例展示了 TypeScript 中接口的合并(Interface Merging)特性。两个同名的 People 接口会自动合并,允许对象 people 实现所有属性(name、age、email),并输出合并后的结果。
// 创建接口 People,包含 name 和 age 属性
interface People {
name: string;
age: number
}
// 重新声明同名接口 People,包含 email 属性,接口会自动合并
interface People {
email: string;
}
// 创建符合合并后接口 People 的对象 people,包含 name、age 和 email 属性
const people: People = {
name: "FelixLu",
age: 20,
email: "FelixLu@gmail.com"
};
// 输出 people 对象
console.log(people);
输出:
{ name: 'FelixLu', age: 20, email: 'FelixLu@gmail.com' }
(三) TypeScript 中 Type 和 Interface 的区别
特性 |
Type |
Interface |
定义 |
一组数据类型 |
一种语法契约 |
灵活性 |
更灵活 |
相较于 Type 灵活性较低 |
关键字 |
使用 |
使用 |
命名 |
支持为类型创建新的名称 |
用于定义实体 |
能力 |
能力较少 |
能力更强 |
对象使用 |
本身不直接支持对象的使用 |
支持对象的使用 |
声明合并 |
不支持多重声明合并 |
支持多重声明合并 |
名称冲突 |
同名的两个类型会报异常 |
同名的两个接口会被合并 |
实现 |
不用于实现目标 |
用于类的实现和继承 |
联合类型 |
不支持实现或继承联合类型 |
支持实现和继承联合类型 |
交叉类型 |
可以通过组合多个类型创建交叉类型 |
不支持创建交叉接口 |
原始类型、联合类型和元组的使用 |
可用于原始类型、联合类型和元组 |
不支持其他类型的声明 |
虽然 TypeScript 的 type 和 interface 在某些特性上有所不同,但它们在很多方面是相似的,且彼此并不能完全取代。开发者可以根据项目的具体需求选择使用哪一种。type 和 interface 各自的灵活性和特定使用场景,使它们成为 TypeScript 开发中非常有价值的工具。
三、接口与类
在 TypeScript 中,接口定义了类必须遵守的结构,确保对象形状的一致性并促进类型检查。
- 接口声明属性和方法,但不包含具体实现,作为类必须实现的契约。
- 类通过 implements 关键字实现接口,并为接口中声明的成员提供具体实现。
以下是相关的方法:
(一) 类实现接口
在 TypeScript 中,类可以实现接口,以确保它遵守接口定义的特定结构。
语法:
class ClassName implements InterfaceName {
// 类的属性和方法
}
一个例子:
// 定义接口 Shape,声明一个方法 calculateArea,返回数字类型
interface Shape {
calculateArea(): number;
}
// Rectangle 类实现 Shape 接口
class Rectangle implements Shape {
width: number;
height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
// 实现接口中的 calculateArea 方法,计算矩形面积
calculateArea(): number {
return this.width * this.height;
}
}
// 创建 Rectangle 实例,并调用 calculateArea 方法
const rect = new Rectangle(5, 10);
console.log(rect.calculateArea());
- Shape 接口定义了一个包含 calculateArea 方法的契约。
- Rectangle 类实现了 Shape 接口,为 calculateArea 方法提供了具体实现。
- 创建了一个宽为 5、高为 10 的 Rectangle 实例,并调用 calculateArea 方法计算面积。
输出:
50
(二) 类实现多个接口
在 TypeScript 中,类可以实现多个接口,从而遵守多个契约,并确保为所有指定的成员提供实现。
语法:
class ClassName implements Interface1, Interface2 {
// 类的属性和方法
}
一个例子:
// 定义接口 Shape,声明方法 calculateArea,返回数字类型
interface Shape {
calculateArea(): number;
}
// 定义接口 Color,声明属性 color,类型为字符串
interface Color {
color: string;
}
// Circle 类同时实现 Shape 和 Color 两个接口
class Circle implements Shape, Color {
radius: number;
color: string;
constructor(radius: number, color: string) {
this.radius = radius;
this.color = color;
}
// 实现接口 Shape 中的 calculateArea 方法,计算圆的面积
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}
// 创建 Circle 实例,半径为 5,颜色为红色
const circle = new Circle(5, 'red');
console.log(`颜色: ${circle.color}`);
console.log(`面积: ${circle.calculateArea()}`);
- Shape 接口声明了一个 calculateArea 方法,定义了计算图形面积的契约。
- Color 接口包含一个 color 属性,规定实现该接口的类必须具有颜色属性。
- Circle 类同时实现了 Shape 和 Color 接口,为 calculateArea 方法和 color 属性提供了具体实现。
输出:
颜色: red
面积: 78.53981633974483
四、创建带条件类型的接口
TypeScript 中的条件类型使开发者能够创建根据所遇类型自动调整行为的接口。该特性提升了代码的灵活性和适应性,尤其适用于类型可能变化的场景。
通过使用条件类型,开发者可以定义动态调整结构和约束的接口,为构建健壮且类型安全的应用提供强大工具。
(一) 使用 extends 关键字
这种方法通过使用条件类型来区分不同的类型,并根据条件有选择地扩展接口的属性。
语法:
interface InterfaceName<T> {
propertyName:
T extends ConditionType ? TypeIfTrue : TypeIfFalse;
}
下面的代码使用 extends 关键字和三元运算符创建了一个带条件类型的接口。
// 定义带有条件类型的接口 Vehicle,泛型参数 T
interface Vehicle<T> {
// 如果 T 是 'car',则 type 属性为 'four-wheeler',否则为 'two-wheeler'
type: T extends 'car' ? 'four-wheeler' : 'two-wheeler';
// 如果 T 是 'car',则 wheels 属性为 4,否则为 2
wheels: T extends 'car' ? 4 : 2;
}
// 创建类型为 Vehicle<'car'> 的对象 car
const car: Vehicle<'car'> = {
type: 'four-wheeler',
wheels: 4
};
// 创建类型为 Vehicle<'bike'> 的对象 bike
const bike: Vehicle<'bike'> = {
type: 'two-wheeler',
wheels: 2
};
console.log(car);
console.log(bike);
输出:
{ type: 'four-wheeler', wheels: 4 }
{ type: 'two-wheeler', wheels: 2 }
(二) 映射类型
映射类型结合条件约束,能够根据特定条件将已有类型转换成新的接口。
语法:
type MappedTypeName<T> = {
[P in T]:
T extends ConditionType ? TypeIfTrue : TypeIfFalse;
};
下面的代码使用映射类型实现了带条件类型的接口。
// 定义联合类型 Fruit,包含 'apple' 和 'banana'
type Fruit = 'apple' | 'banana';
// 定义映射类型 FruitProperties,泛型参数 T 是字符串数组
// 遍历数组中的每个元素 P,若 P 是 'apple',属性类型为 { color: string }
// 否则属性类型为 { length: number }
type FruitProperties<T extends string[]> = {
[P in T[number]]: P extends 'apple' ? { color: string } : { length: number };
};
// FruitInfo 接口继承自 FruitProperties,传入 Fruit 数组类型
interface FruitInfo extends FruitProperties<Fruit[]> {}
// 创建 FruitInfo 类型的 appleInfo 对象,包含 apple 属性
const appleInfo: FruitInfo = {
apple: { color: 'red' }
} as FruitInfo;
// 创建 FruitInfo 类型的 bananaInfo 对象,包含 banana 属性
const bananaInfo: FruitInfo = {
banana: { length: 10 }
} as FruitInfo;
console.log(appleInfo);
console.log(bananaInfo);
输出:
{ apple: { color: red } }
{ banana: { length: 10 } }
(三) 条件类型和联合类型
在这种方法中,我们结合联合类型和条件类型,定义能够根据不同类型动态调整的接口。
type ConditionalInterface<T> = T extends ConditionType1 ? InterfaceType1 :
T extends ConditionType2 ? InterfaceType2 :
DefaultInterfaceType;
下面的代码演示了如何结合条件类型和联合类型,动态定义接口。
type Fruit = 'apple' | 'banana';
// 定义条件类型 FruitProperties:根据不同的 T 类型返回不同的属性结构
type FruitProperties<T> =
T extends 'apple' ? { color: string } :
T extends 'banana' ? { length: number } :
never;
// 创建 appleProperties,类型为 FruitProperties<'apple'>,要求包含 color 属性
const appleProperties: FruitProperties<'apple'> = { color: 'red' };
// 创建 bananaProperties,类型为 FruitProperties<'banana'>,要求包含 length 属性
const bananaProperties: FruitProperties<'banana'> = { length: 10 };
console.log(appleProperties);
console.log(bananaProperties);
输出:
{ color: 'red' }
{ length: 10 }
五、混合类型
在 TypeScript 中,混合类型(Hybrid Types)指的是将不同类型特性组合在一起的类型,例如对象、函数、数组等的结合体。混合类型可以让一个值既像函数一样被调用,又像对象一样拥有属性和方法。
在 TypeScript 中,我们可以使用接口(interface
)或类型别名(type alias
)来定义混合类型(Hybrid Types)。这些声明可以包括:
- 属性签名(Property Signatures):声明对象类型的属性;
- 调用签名(Call Signatures):声明该对象可以像函数一样被调用;
- 索引签名(Index Signatures):允许对象通过键访问其成员。
混合类型通常是以下不同类型元素的组合,包括:
- 可调用属性(Callable properties):该对象可以作为函数被调用;
- 属性(Properties):该对象像普通对象一样拥有属性。
这使得我们可以定义出功能灵活、可配置的对象类型,既可以调用也可以持有状态和方法。
(一) 场景示例
1. 对象-函数混合类型
在下面的示例中,Logger
接口引入了两种函数类型的混合:一种是接收消息作为参数的函数,另一种是不接收参数的函数。此外,它还暴露了一个名为 level
的属性和一个名为 setLevel
的方法。
interface Logger {
(message: string): void; // 可调用签名:接受一个字符串消息作为参数
level: string; // 属性:日志级别
setLevel(newLevel: string): void; // 方法:设置日志级别
}
function createLogger(): Logger {
let logger = function (message: string) {
console.log(`[${logger.level}] ${message}`);
} as Logger;
logger.level = "info"; // 默认级别为 info
logger.setLevel = function (newLevel: string) {
logger.level = newLevel; // 设置新的日志级别
};
return logger;
}
const myLogger = createLogger();
myLogger("启动应用程序。");
myLogger.setLevel("debug");
myLogger("调试应用程序。");
输出:
[info] 启动应用程序。
[debug] 调试应用程序。
2. 数组-对象混合类型
在这个例子中,StringArray
接口既是一种数组,也是一种对象。这意味着它允许使用数字进行索引,并具有像 length
和 description
这样的附加属性。
interface StringArray {
[index: number]: string; // 使用数字索引,返回字符串
length: number; // 表示数组的长度
description: string; // 描述信息
}
let myStringArray: StringArray = {
0: "hello",
1: "world",
length: 2,
description: "这个数组包含字符串。" // 中文翻译
};
console.log(myStringArray[0]);
console.log(myStringArray.description);
输出:
hello
这个数组包含字符串。
(二) 使用场景和优点
1. 混合类型的使用场景
- 库 API:库应使用混合类型来创建灵活且健壮的 API;例如,jQuery 对象既可以作为函数调用,也有方法和属性。
- 事件触发器:它可能包含用于触发事件的函数,以及添加或移除监听器的方法。
- 配置对象:配置对象可以是可调用的(即触发某些操作),同时也包含配置属性。
- 复杂数据结构:混合类型非常适合需要支持类似对象的动态键和类似数组的数字索引的复杂数据结构。
2. 混合类型的优点
- 灵活性:允许一个类型扮演多种角色,从而简化了适应性强的 API 的创建。
- 类型安全:TypeScript 的类型检查可以在编译时捕获混合类型的错误。