Skip to content

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

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

什么是接口?

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

其基本语法非常直观:

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

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

  • 一个 User 对象必须有一个只读的 id 属性,类型为 number
  • 必须有一个 usernameemail 属性,类型为 string
  • 可以有一个可选的 avatarUrl 属性,如果存在,其类型必须是 string

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

typescript
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 属性。

typescript
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 UserAdmin 接口自动继承了 User 的所有属性(id, username, email, avatarUrl?),并在此基础上添加了自己的 level 属性。这种方式极大地促进了代码的复用和层次化设计。

interface vs. type: 终极对决

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

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

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

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

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 无法做到的。

typescript
// 联合类型
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 是定义对象和类结构蓝图的首选工具,它通过 extends 关键字支持清晰的继承关系。
  • 其独特的“声明合并”特性,使其在扩展已有类型定义时无可替代。
  • 虽然 type 在某些方面更为灵活,但在定义核心业务对象的“形状”时,interface 提供了更符合面向对象思想的语义和实践。

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

不知道说啥了很无语了