在 TypeScript 的世界里,如果说类型别名 (type) 是一位灵活的魔术师,能为各种类型赋予新名字,那么接口 (interface) 则更像是一位严谨的建筑师,专门为对象的结构蓝图绘制契约。
接口是 TypeScript 核心概念之一,也是其面向对象特性的重要基石。它允许我们定义一个“契约”,任何被该契约“标记”的对象或类都必须遵守其规定。这种强制性的约定,是构建大型、可维护和可协作应用的关键。
什么是接口?
接口的核心任务是定义一个对象应该具有的形状。它描述了对象必须包含哪些属性,以及这些属性的类型。任何试图“扮演”这个角色的对象,都必须严格遵守这份契约。
其基本语法非常直观:
interface User {
readonly id: number;
username: string;
email: string;
avatarUrl?: string; // 可选属性
}这个 User 接口定义了一个清晰的契约:
- 一个
User对象必须有一个只读的id属性,类型为number。 - 必须有一个
username和email属性,类型为string。 - 可以有一个可选的
avatarUrl属性,如果存在,其类型必须是string。
我们可以像使用其他类型一样使用这个接口:
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?
根据社区的最佳实践,我们可以遵循以下准则:
优先使用
interface来定义对象和类的结构。- 当你需要定义一个明确的对象“形状”,并且期望它可能被其他接口
extends或被类implements时,interface是更符合语义和面向对象思想的选择。 - 当你需要利用声明合并来扩展一个已有的接口时(例如扩展第三方库的类型定义),
interface是唯一的选择。
- 当你需要定义一个明确的对象“形状”,并且期望它可能被其他接口
在需要更灵活的类型组合时,使用
type。- 当你需要定义联合类型、元组、或映射类型等非对象结构时,必须使用
type。 - 当你想为一个已有类型(即使是原始类型)创建一个清晰的别名时,
type是最直接的方式。
- 当你需要定义联合类型、元组、或映射类型等非对象结构时,必须使用
一个广为流传的经验法则是:“用 interface 直到你需要 type 的特性为止”。这鼓励我们默认使用 interface 来保持代码风格的一致性,只在遇到 interface 无法处理的场景(如联合类型)时才切换到 type。
总结
接口是 TypeScript 类型系统的支柱,它为代码带来了“契约精神”,使得大规模协作和长期维护成为可能。
interface是定义对象和类结构蓝图的首选工具,它通过extends关键字支持清晰的继承关系。- 其独特的“声明合并”特性,使其在扩展已有类型定义时无可替代。
- 虽然
type在某些方面更为灵活,但在定义核心业务对象的“形状”时,interface提供了更符合面向对象思想的语义和实践。
深刻理解 interface 和 type 的异同,并根据场景做出明智的选择,是每一位 TypeScript 开发者迈向更高阶水平的必经之路。
