第4章 接口:定义代码的"形状契约"
想象你正在组装一台电脑——主板上的每个接口(USB、HDMI、电源插槽)都严格定义了形状和尺寸。在TypeScript中,接口(Interface) 就是这样的存在:它明确规定了一个对象应该有哪些"插槽"(属性),每个插槽应该是什么"形状"(类型)。这一章,我们将学会如何用接口打造严丝合缝的代码结构!
4.1 接口基础——给对象画"设计蓝图"
接口最简单的用法就是定义对象的形状(Shape),就像建筑师绘制房屋蓝图一样精确。
📋 基础接口定义
// 定义用户接口 - 就像产品设计文档
interface UserProfile {
username: string; // 必须属性
age: number; // 必须属性
email?: string; // 可选属性(后面详解)
}
// 按照接口"生产"对象
const newUser: UserProfile = {
username: "日落",
age: 18,
email: "coder@example.com"
};
// 错误示例:缺少必需属性
const invalidUser: UserProfile = {
username: "错误示范"
// 缺少age属性,TS报错!
};
// 错误示例:添加未定义的属性
const extraPropUser: UserProfile = {
username: "张三",
age: 30,
hobby: "编程" // 错误!接口未定义hobby属性
};
💡 核心思想:接口是类型检查的模板,确保对象包含必要的属性和正确的类型,就像质检员对照图纸检查产品。
4.2 可选属性与只读属性——灵活的"装配选项"
TypeScript接口提供了两种特殊的属性修饰符,让你的类型定义更加精确和安全。
❓ 可选属性 - 打上问号标记
可选属性就像汽车的"选装配置"——有更好,没有也不影响基本功能。
何时使用可选属性?
- 配置对象:某些配置项可能有默认值
- API响应:服务器可能不返回某些字段
- 用户输入:表单中的非必填项
- 渐进式构建:对象可能分步骤完善
interface Config {
maxConnections: number;
timeout?: number; // 可选属性
retryCount?: number;
}
// 合法:不提供可选属性
const configA: Config = { maxConnections: 10 };
// 合法:提供部分可选属性
const configB: Config = {
maxConnections: 5,
timeout: 3000
};
// 访问可选属性需做安全检查
if (configA.timeout) {
console.log(`超时设置:${configA.timeout}ms`);
} else {
console.log("使用默认超时设置");
}
🔒 只读属性 - 加上readonly锁
只读属性就像"出厂设置"——一旦初始化就不能修改,保证数据的不可变性。
只读属性的应用场景
- 配置常量:如API端点、版本号
- 身份标识:如用户ID、订单号
- 时间戳:如创建时间、修改时间
- 防篡改数据:关键业务数据保护
interface Point {
readonly x: number; // 初始化后不可修改
readonly y: number;
}
const origin: Point = { x: 0, y: 0 };
origin.x = 100; // 错误!只读属性不可修改
// 只读数组:防止数组被修改
const fixedArray: ReadonlyArray<number> = [1, 2, 3];
fixedArray[0] = 99; // 错误!
fixedArray.push(4); // 错误!
// 实际应用:API配置
interface ApiConfig {
readonly baseUrl: string;
readonly version: string;
timeout?: number;
}
const apiConfig: ApiConfig = {
baseUrl: "https://api.example.com",
version: "v1.0",
timeout: 5000
};
// apiConfig.baseUrl = "hack"; // 错误!无法修改
apiConfig.timeout = 3000; // 正确!非只读属性可修改
🛡️ 安全提醒:只读属性只在编译时检查,运行时仍可能被修改。对于真正的不可变性,考虑使用
Object.freeze()。
4.3 索引签名——处理"未知属性"的万能插槽
当对象可能有多个未知属性时,索引签名(Index Signature)就像万能插槽一样派上用场。
🔧 基础索引签名
// 定义字典接口
interface StringDictionary {
[key: string]: string; // 键为字符串,值为字符串
}
// 合法使用
const colors: StringDictionary = {
red: "#FF0000",
green: "#00FF00"
};
// 动态添加属性
colors["blue"] = "#0000FF";
colors.yellow = "#FFFF00"; // 也可以用点语法
// 错误示例:值类型错误
colors["error"] = 123; // 应为string!
🎭 混合接口:已知属性 + 索引签名
// 混合已知和未知属性
interface HybridDictionary {
name: string; // 已知属性
version: number; // 已知属性
[key: string]: string | number; // 万能插槽
}
const data: HybridDictionary = {
name: "数据包",
version: 1,
size: 1024, // 数值类型允许
format: "json", // 字符串类型允许
author: "RiLuo" // 字符串类型允许
};
⚠️ 索引签名注意事项
- 类型兼容性:索引签名的类型必须包含所有显式属性的类型
- 键类型限制:键类型只能是
string、number或symbol - 唯一性约束:同一对象只能有一种索引签名
// 错误示例:类型不兼容
interface BadInterface {
name: string;
[key: string]: number; // 错误!name是string,但索引签名要求number
}
// 正确示例:类型兼容
interface GoodInterface {
name: string;
age: number;
[key: string]: string | number; // 正确!包含了name和age的类型
}
4.4 函数类型接口——定义"方法模板"
接口不仅能描述对象结构,还能描述函数的"形状"——参数类型和返回值类型。
🎯 函数签名定义
// 定义搜索函数接口
interface SearchFunc {
(source: string, keyword: string): boolean;
}
// 实现接口
const mySearch: SearchFunc = (src, kw) => {
return src.includes(kw);
};
// 参数名无需匹配接口
const yourSearch: SearchFunc = (s, k) => s.indexOf(k) > -1;
// 错误示例:返回值类型不匹配
const wrongSearch: SearchFunc = (src, kw) => {
return "找到结果"; // 错误!应为boolean
};
🚀 高级函数接口应用
// 事件处理器接口
interface EventHandler<T> {
(event: T): void;
}
// 数据转换器接口
interface DataTransformer<TInput, TOutput> {
(input: TInput): TOutput;
}
// 实际应用
const clickHandler: EventHandler<MouseEvent> = (e) => {
console.log(`点击位置:${e.clientX}, ${e.clientY}`);
};
const stringToNumber: DataTransformer<string, number> = (str) => {
return parseInt(str, 10);
};
✨ 应用场景:
- 回调函数定义:统一回调函数签名
- 事件处理器:规范事件处理逻辑
- 工具函数:定义通用工具函数接口
- 插件系统:规范插件接口
4.5 类类型接口——强制实现"规范"
类类型接口让类必须满足特定结构,就像制定行业标准一样确保实现的一致性。
🏗️ 基础类接口实现
// 定义闹钟接口
interface Alarm {
alert(): void; // 必须实现的方法
}
// 定义门接口
interface Door {
open(): void;
close(): void;
}
// 实现多个接口
class SecurityDoor implements Door, Alarm {
open() { console.log("门开了"); }
close() { console.log("门关了"); }
alert() { console.log("滴滴滴!有人闯入!"); }
}
// 错误示例:缺少方法实现
class BrokenAlarm implements Alarm {
// 缺少alert方法,TS报错!
}
// 实际使用
const myDoor = new SecurityDoor();
myDoor.open(); // "门开了"
myDoor.alert(); // "滴滴滴!有人闯入!"
🎪 接口的重要特性
- 公共契约:类需要实现接口中定义的所有公共方法
- 实例检查:接口只检查实例部分(不检查构造函数和静态方法)
- 多重实现:一个类可以实现多个接口
- 灵活组合:通过接口组合实现复杂功能
// 复杂示例:智能设备
interface Connectable {
connect(): void;
disconnect(): void;
}
interface Controllable {
turnOn(): void;
turnOff(): void;
}
interface Monitorable {
getStatus(): string;
}
// 智能灯泡:实现所有接口
class SmartBulb implements Connectable, Controllable, Monitorable {
private isConnected = false;
private isOn = false;
connect() {
this.isConnected = true;
console.log("灯泡已连接WiFi");
}
disconnect() {
this.isConnected = false;
console.log("灯泡已断开连接");
}
turnOn() {
if (this.isConnected) {
this.isOn = true;
console.log("灯泡已开启");
}
}
turnOff() {
this.isOn = false;
console.log("灯泡已关闭");
}
getStatus() {
return `连接状态:${this.isConnected ? '已连接' : '未连接'},开关状态:${this.isOn ? '开启' : '关闭'}`;
}
}
4.6 接口继承——构建"接口家族树"
接口可以相互继承,形成层次结构,就像生物分类学一样建立清晰的类型谱系。
🌳 单继承:线性扩展
// 基础接口
interface Animal {
name: string;
eat(): void;
}
// 继承Animal
interface Mammal extends Animal {
warmBlooded: boolean;
run(): void;
}
// 多层继承
interface Dog extends Mammal {
bark(): void;
}
// 实现最终接口
class Labrador implements Dog {
name = "拉布拉多";
warmBlooded = true;
eat() { console.log("啃骨头"); }
run() { console.log("四条腿奔跑"); }
bark() { console.log("汪汪!"); }
}
🔗 多继承:组合能力
// 多个基础接口
interface Shape {
color: string;
}
interface Border {
borderWidth: number;
}
interface Clickable {
onClick(): void;
}
// 同时继承多个接口
interface Button extends Shape, Border, Clickable {
text: string;
}
// 实现复合接口
const myButton: Button = {
color: "blue",
borderWidth: 2,
text: "点击我",
onClick() {
console.log("按钮被点击了!");
}
};
🎯 继承的核心优势
- 代码复用:避免重复定义相同属性
- 结构清晰:建立类型层次关系
- 灵活组合:通过多继承创建复杂接口
- 类型约束:子接口自动包含父接口要求
- 维护性强:修改基础接口自动影响所有子接口
4.7 接口实战:智能设备管理系统
让我们通过一个完整的实战案例,综合运用本章学到的所有接口技巧:
// 定义基础设备接口
interface BaseDevice {
readonly id: string;
readonly name: string;
readonly type: "sensor" | "actuator" | "controller";
isOnline: boolean;
lastUpdate: Date;
}
// 传感器接口
interface Sensor extends BaseDevice {
type: "sensor";
readValue(): number;
getUnit(): string;
}
// 执行器接口
interface Actuator extends BaseDevice {
type: "actuator";
execute(command: string): boolean;
getStatus(): string;
}
// 可配置设备接口
interface Configurable {
config: { [key: string]: any };
updateConfig(newConfig: Partial<{ [key: string]: any }>): void;
}
// 智能温度传感器:组合多个接口
class SmartTempSensor implements Sensor, Configurable {
readonly id = "temp-001";
readonly name = "智能温度传感器";
readonly type = "sensor" as const;
isOnline = true;
lastUpdate = new Date();
config = {
unit: "celsius",
precision: 1,
interval: 5000
};
readValue(): number {
// 模拟读取温度
return Math.round((Math.random() * 30 + 10) * 10) / 10;
}
getUnit(): string {
return this.config.unit === "celsius" ? "°C" : "°F";
}
updateConfig(newConfig: Partial<{ [key: string]: any }>): void {
this.config = { ...this.config, ...newConfig };
this.lastUpdate = new Date();
}
}
// 设备管理器接口
interface DeviceManager {
devices: BaseDevice[];
addDevice(device: BaseDevice): void;
removeDevice(id: string): boolean;
getDevice(id: string): BaseDevice | undefined;
getOnlineDevices(): BaseDevice[];
}
// 实现设备管理器
class IoTDeviceManager implements DeviceManager {
devices: BaseDevice[] = [];
addDevice(device: BaseDevice): void {
this.devices.push(device);
console.log(`设备 ${device.name} 已添加`);
}
removeDevice(id: string): boolean {
const index = this.devices.findIndex(d => d.id === id);
if (index > -1) {
this.devices.splice(index, 1);
return true;
}
return false;
}
getDevice(id: string): BaseDevice | undefined {
return this.devices.find(d => d.id === id);
}
getOnlineDevices(): BaseDevice[] {
return this.devices.filter(d => d.isOnline);
}
}
// 使用示例
const manager = new IoTDeviceManager();
const tempSensor = new SmartTempSensor();
manager.addDevice(tempSensor);
console.log(`当前温度:${tempSensor.readValue()}${tempSensor.getUnit()}`);
// 更新配置
tempSensor.updateConfig({ unit: "fahrenheit", precision: 2 });
console.log(`配置更新后温度:${tempSensor.readValue()}${tempSensor.getUnit()}`);
4.8 接口 vs 类型别名——如何明智选择?
在TypeScript中,接口和类型别名都能描述对象形状,但各有所长:
📊 特性对比表
| 特性 | 接口 (interface) | 类型别名 (type) | |
|---|---|---|---|
| 继承 | 使用extends扩展 | 使用&交叉类型 | |
| 合并 | 同名接口自动合并 | 同名类型会冲突 | |
| 实现类 | 可用implements | 不能用于implements | |
| 描述对象 | 更适合 | 适合 | |
| 描述联合类型 | 不能 | 更适合 (`type = A | B`) |
| 元组 | 能描述但不够直观 | 更直观 (type T = [A, B]) | |
| 计算属性 | 不支持 | 支持映射类型 |
🎯 选择指南
优先使用接口的场景:
- 定义对象的公共API
- 需要类实现的契约
- 可能需要扩展的类型
- 团队协作中的类型规范
优先使用类型别名的场景:
- 联合类型:
type Status = "loading" | "success" | "error" - 元组类型:
type Point = [number, number] - 复杂的类型计算和映射
- 基础类型的语义化命名
// 接口适用场景
interface UserAPI {
getUser(id: string): Promise<User>;
updateUser(id: string, data: Partial<User>): Promise<User>;
}
class UserService implements UserAPI {
// 实现接口方法...
}
// 类型别名适用场景
type Theme = "light" | "dark" | "auto";
type Coordinates = [number, number];
type EventCallback<T> = (event: T) => void;
本章核心收获
通过这一章的学习,你已经掌握了TypeScript接口系统的精髓:
- 接口即契约:确保对象符合预定形状,提供编译时类型安全
- 灵活控制:可选属性和只读属性提供精细的类型控制
- 动态扩展:索引签名优雅处理开放式对象结构
- 行为规范:函数接口和类接口强制实现一致的行为
- 继承体系:通过继承构建清晰的类型层次结构
- 组合艺术:多接口实现创造强大的复合类型
- 工具选择:明确接口与类型别名的适用场景
成就解锁:你已经掌握了用接口塑造代码结构的艺术!下一章我们将深入类与面向对象编程,学习如何用类实现这些接口契约,构建更加复杂和强大的应用架构。准备好进入OOP的精彩世界了吗?