Skip to content

TypeScript 入门到实战(三):数据结构化 —— Interface 与 Type 的深度对决


在前两篇文章中,我们掌握了 TypeScript 的基础类型和函数用法。我们学会了如何给单个变量(string, number)和函数的出入参加上类型“护栏”。但现实世界的数据远比这复杂。我们经常需要处理包含多个字段的复杂对象,比如一个用户、一件商品或一篇文章。

在第二章中,我们尝试用 let person: object = { ... } 来注解一个对象,结果发现它虽然能保证 person 是一个对象,却无法告诉我们对象内部的形状(Shape)

为了精确地描述对象的结构,TypeScript 提供了两个强大的工具:interface (接口) 和 type (类型别名)。它们是 TypeScript 的核心,也是许多开发者(包括面试官)最喜欢探讨的话题。今天,我们就来一场深度对决,彻底搞懂它们。

1. 使用 interface 定义对象蓝图

interface 是 TypeScript 中用于定义对象结构的老牌功臣。你可以把它想象成一个对象的“蓝图”或“契约”,任何声称符合该接口的对象,都必须拥有接口所要求的所有属性和方法。

让我们来为一个用户对象创建一个蓝图:

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
};

// 如果缺少属性或者类型不匹配,TS会立刻报错
// const wrongUser: User = { id: 2, name: "Bob" }; 
// 错误: 类型 "{ id: number; name: string; }" 中缺少属性 "email",但类型 "User" 中需要该属性。

可选属性 (?) 与只读属性 (readonly)

有时候,对象的某些属性不是必需的。我们可以用 ? 来标记它们为可选属性。而对于像 id 这样不希望被修改的属性,我们可以用 readonly 来修饰。

typescript
interface User {
  readonly id: number; // id 在创建后就不能被修改
  name: string;
  email: string;
  bio?: string; // bio 是可选的
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
};

user.name = "Alice Smith"; // OK
// user.id = 2; // 错误: 无法为“id”赋值,因为它是只读属性。

接口继承 (extends)

interface 之间可以像类一样继承,这使得复用和扩展类型定义变得非常容易。比如,我们可以在 User 的基础上创建一个 Admin 接口。

typescript
interface Admin extends User {
  level: number;
}

const admin: Admin = {
  id: 100,
  name: "Super Admin",
  email: "admin@example.com",
  level: 1, // Admin 必须有 level 属性
  // bio 依然是可选的
};

2. 使用 type 创建类型别名

type,即类型别名,顾名思义,就是给一个类型起一个新的名字。它的功能非常强大,不仅能定义对象结构,还能做更多的事情。

首先,type 完全可以做到 interface 刚才做的所有事:

typescript
type UserType = {
  readonly id: number;
  name: string;
  email: string;
  bio?: string;
};

// "扩展" type 需要使用交叉类型 (&)
type AdminType = UserType & {
  level: number;
};

const user2: UserType = { id: 2, name: "Bob", email: "bob@example.com" };

看到这里,你一定满头问号:既然功能如此相似,为什么需要两个东西?别急,type 的“独门绝技”在于它能为任何类型创建别名,而不仅仅是对象。

typescript
// 为联合类型创建别名
type ID = string | number;

// 为原始类型创建别名
type StatusCode = 200 | 404 | 500;

// 为元组类型创建别名
type Point = [number, number];

const userId: ID = "user-123";
const httpStatus: StatusCode = 404;
const origin: Point = [0, 0];

这些是 interface 无法做到的。

3. 终极对决:Interface vs Type

这是本文的核心。让我们把它们的异同点放在一起进行比较。

相同点

  • 都可以用来描述对象的形状或函数签名。
  • 都支持扩展(interface 使用 extendstype 使用交叉类型 &)。

不同点

特性interfacetype
声明合并支持。多个同名 interface 会自动合并。不支持。同名 type 会直接报错。
定义范围只能定义对象、类、函数的结构。可以定义任何类型,包括联合类型、交叉类型、元组、原始类型等。
实现 (implements)✅ 类可以 implements 一个或多个 interface✅ 类也可以 implements 一个 type(该 type 必须是对象结构),但 interface 在此场景更符合惯例。
命名通常作为“契约”或“蓝图”存在,首字母大写。通常用于创建“别名”,更灵活。

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

这是 interface 最独特的特性。当你多次声明同一个 interface 时,它们的所有属性会合并到一起。

typescript
interface Window {
  title: string;
}

// 在项目的其他地方,你可能需要为 Window 对象添加新的属性
interface Window {
  ts: any; // 比如 TypeScript 官方 Playground 添加的属性
}

// 现在,Window 接口同时拥有 title 和 ts 两个属性
const myWindow: Window = { title: "My App", ts: {} };

这个特性使得 interface 非常适合用来扩展第三方库或原生对象的类型定义。而 type 不支持声明合并,这保证了类型的封闭性。

4. 实践建议:到底该用哪个?

现在,我们来回答这个终极问题。社区中存在不同的声音,但以下是一个被广泛接受且非常实用的准则:

  1. 优先使用 interface 来定义数据结构:当你需要描述一个对象或一个类的结构时,优先使用 interface。它的 extends 语法更直观,implements 类的行为更符合面向对象的编程习惯,并且“声明合并”的特性在某些场景下非常有用。

  2. interface 无法满足时,再使用 type:如果你需要定义联合类型、交叉类型、元组,或者想给一个复杂的类型起个别名,那么 type 就是你的不二之选。

黄金法则: “能在 interface 里实现的,就在 interface 里实现;不能的,再用 type。”

最重要的法则: 保持团队风格统一。无论你和你的团队选择哪种风格作为主导,请在整个项目中保持一致。

总结与展望

今天,我们深入了解了 TypeScript 中用于数据建模的两个核心工具:interfacetype。我们不仅学会了如何使用它们来定义从简单到复杂的各种数据结构,更重要的是,我们厘清了它们之间的关键差异,并获得了一套清晰的决策指南。

现在,你已经具备了为项目中的任何数据建立精确“蓝图”的能力。

但新的问题又来了:如果我们想写一个函数,它能接收多种不同结构的数据,但又能保持各自的类型信息,该怎么办呢?比如,一个函数接收一个数组,返回数组的第一项,我们希望如果传入的是 number[],返回的就是 number;如果传入的是 string[],返回的就是 string

这就引出了 TypeScript 中另一个极具魔力的概念——泛型(Generics)。我们将在下一篇文章**《TypeScript 入门到实战(四):代码复用与抽象 —— 泛型 (Generics) 的魔力》**中一探究竟。准备好迎接真正的类型编程魔法吧!