TypeScript入门(四)接口:定义代码的"形状契约"

105 阅读4分钟

第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"   // 字符串类型允许
};

⚠️ 索引签名注意事项

  1. 类型兼容性:索引签名的类型必须包含所有显式属性的类型
  2. 键类型限制:键类型只能是 stringnumbersymbol
  3. 唯一性约束:同一对象只能有一种索引签名
// 错误示例:类型不兼容
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);
};

✨ 应用场景

  1. 回调函数定义:统一回调函数签名
  2. 事件处理器:规范事件处理逻辑
  3. 工具函数:定义通用工具函数接口
  4. 插件系统:规范插件接口

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("按钮被点击了!");
  }
};

🎯 继承的核心优势

  1. 代码复用:避免重复定义相同属性
  2. 结构清晰:建立类型层次关系
  3. 灵活组合:通过多继承创建复杂接口
  4. 类型约束:子接口自动包含父接口要求
  5. 维护性强:修改基础接口自动影响所有子接口

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 = AB`)
元组能描述但不够直观更直观 (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接口系统的精髓:

  1. 接口即契约:确保对象符合预定形状,提供编译时类型安全
  2. 灵活控制:可选属性和只读属性提供精细的类型控制
  3. 动态扩展:索引签名优雅处理开放式对象结构
  4. 行为规范:函数接口和类接口强制实现一致的行为
  5. 继承体系:通过继承构建清晰的类型层次结构
  6. 组合艺术:多接口实现创造强大的复合类型
  7. 工具选择:明确接口与类型别名的适用场景

成就解锁:你已经掌握了用接口塑造代码结构的艺术!下一章我们将深入类与面向对象编程,学习如何用类实现这些接口契约,构建更加复杂和强大的应用架构。准备好进入OOP的精彩世界了吗?