11.接口 (Interfaces) 副本.md

查看飞书原文 ↗4,407 字符

在 TypeScript 的世界里,如果说类型别名 (type) 是一位灵活的魔术师,能为各种类型赋予新名字,那么接口 (interface) 则更像是一位严谨的建筑师,专门为对象的结构蓝图绘制契约。

接口是 TypeScript 核心概念之一,也是其面向对象特性的重要基石。它允许我们定义一个“契约”,任何被该契约“标记”的对象或类都必须遵守其规定。这种强制性的约定,是构建大型、可维护和可协作应用的关键。

什么是接口?

接口的核心任务是定义一个对象应该具有的形状。它描述了对象必须包含哪些属性,以及这些属性的类型。任何试图“扮演”这个角色的对象,都必须严格遵守这份契约。

其基本语法非常直观:

interface User {
  readonly id: number;
  username: string;
  email: string;
  avatarUrl?: string; // 可选属性
}

这个 User 接口定义了一个清晰的契约:

我们可以像使用其他类型一样使用这个接口:

function displayUser(user: User): void {
  console.log(`Username: ${user.username}`);
  console.log(`Email: ${user.email}`);
}

const myUser: User = {
  id: 1,
  username: "raycast",
  email: "hello@raycast.com",
};

displayUser(myUser); // 合法

// const invalidUser = { id: 2, username: "ai" }; 
// 编译时错误: Property 'email' is missing in type '{ id: number; username: string; }' 
// but required in type 'User'.

编译器会确保任何传递给 displayUser 函数的参数,或者任何声明为 User 类型的变量,都严格遵守 User 接口所定义的结构。

接口的扩展:extends

接口最强大的特性之一是其可扩展性。一个接口可以通过 extends 关键字来“继承”另一个接口的成员,从而构建出更复杂的类型结构。这在面向对象编程中是非常常见的模式。

假设我们要在 User 的基础上定义一个 Admin 用户,它除了拥有普通用户的所有属性外,还有一个额外的 level 属性。

interface Admin extends User {
  level: 'super' | 'editor' | 'viewer';
}

const myAdmin: Admin = {
  id: 2,
  username: "admin_user",
  email: "admin@raycast.com",
  level: "super",
};

displayUser(myAdmin); // 合法, 因为 Admin 是 User 的一种

通过 extends User,Admin 接口自动继承了 User 的所有属性(id, username, email, avatarUrl?),并在此基础上添加了自己的 level 属性。这种方式极大地促进了代码的复用和层次化设计。

interface vs. type: 终极对决

这是 TypeScript 开发者最常讨论的话题之一。interface 和 type 在定义对象形状方面有很多重叠之处,但它们的设计哲学和关键特性有所不同。

特性 interface (接口) type (类型别名)
核心用途 专门用于定义对象、类、函数的“契约”或“形状” 为任何 TypeScript 类型创建别名,用途更广泛
扩展方式 使用 extends 关键字,清晰地表达继承关系 使用交叉类型 & 来组合类型
声明合并 支持。多个同名 interface 会自动合并,这对于扩展第三方库的类型非常有用。 不支持。创建同名的 type 会直接导致编译错误。
适用范围 主要用于对象结构 可以是联合类型 (string | number)、元组 ([string, number]) 等任何类型

关键区别 1:声明合并 (Declaration Merging)

这是两者之间最本质的区别。你可以多次声明同一个 interface,TypeScript 会将它们的属性合并在一起。

// 在文件 a.ts 中
interface Window {
  title: string;
}

// 在文件 b.ts 中 (同一个项目)
interface Window {
  raycastApi: object;
}

// TypeScript 会将它们合并为:
// interface Window {
//   title: string;
//   raycastApi: object;
// }

window.title = "My App";
window.raycastApi = { /* ... */ };

这个特性使得 interface 成为扩展现有 JavaScript 对象(如 window)或为第三方库添加自定义属性时的理想选择。而 type 别名则不允许这样做,保证了类型的唯一性。

关键区别 2:表达能力

type 别名的表达能力更广泛。它不仅能定义对象,还能为联合类型、元组、交叉类型等任何类型创建别名,这是 interface 无法做到的。

// 联合类型
type Status = "success" | "pending" | "failed";

// 元组
type PointData = [number, number];

// 无法用 interface 实现
// interface Status = "success" | "pending" | "failed"; // 错误!

何时使用 interface?何时使用 type?

根据社区的最佳实践,我们可以遵循以下准则:

  1. 优先使用 interface 来定义对象和类的结构。

    • 当你需要定义一个明确的对象“形状”,并且期望它可能被其他接口 extends 或被类 implements 时,interface 是更符合语义和面向对象思想的选择。
    • 当你需要利用声明合并来扩展一个已有的接口时(例如扩展第三方库的类型定义),interface 是唯一的选择。
  2. 在需要更灵活的类型组合时,使用 type。

    • 当你需要定义联合类型、元组、或映射类型等非对象结构时,必须使用 type。
    • 当你想为一个已有类型(即使是原始类型)创建一个清晰的别名时,type 是最直接的方式。

一个广为流传的经验法则是:“用 interface 直到你需要 type 的特性为止”。这鼓励我们默认使用 interface 来保持代码风格的一致性,只在遇到 interface 无法处理的场景(如联合类型)时才切换到 type。

总结

接口是 TypeScript 类型系统的支柱,它为代码带来了“契约精神”,使得大规模协作和长期维护成为可能。

深刻理解 interface 和 type 的异同,并根据场景做出明智的选择,是每一位 TypeScript 开发者迈向更高阶水平的必经之路。