Appearance
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
使用extends
,type
使用交叉类型&
)。
不同点
特性 | interface | type |
---|---|---|
声明合并 | ✅ 支持。多个同名 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. 实践建议:到底该用哪个?
现在,我们来回答这个终极问题。社区中存在不同的声音,但以下是一个被广泛接受且非常实用的准则:
优先使用
interface
来定义数据结构:当你需要描述一个对象或一个类的结构时,优先使用interface
。它的extends
语法更直观,implements
类的行为更符合面向对象的编程习惯,并且“声明合并”的特性在某些场景下非常有用。当
interface
无法满足时,再使用type
:如果你需要定义联合类型、交叉类型、元组,或者想给一个复杂的类型起个别名,那么type
就是你的不二之选。
黄金法则: “能在
interface
里实现的,就在interface
里实现;不能的,再用type
。”
最重要的法则: 保持团队风格统一。无论你和你的团队选择哪种风格作为主导,请在整个项目中保持一致。
总结与展望
今天,我们深入了解了 TypeScript 中用于数据建模的两个核心工具:interface
和 type
。我们不仅学会了如何使用它们来定义从简单到复杂的各种数据结构,更重要的是,我们厘清了它们之间的关键差异,并获得了一套清晰的决策指南。
现在,你已经具备了为项目中的任何数据建立精确“蓝图”的能力。
但新的问题又来了:如果我们想写一个函数,它能接收多种不同结构的数据,但又能保持各自的类型信息,该怎么办呢?比如,一个函数接收一个数组,返回数组的第一项,我们希望如果传入的是 number[]
,返回的就是 number
;如果传入的是 string[]
,返回的就是 string
。
这就引出了 TypeScript 中另一个极具魔力的概念——泛型(Generics)。我们将在下一篇文章**《TypeScript 入门到实战(四):代码复用与抽象 —— 泛型 (Generics) 的魔力》**中一探究竟。准备好迎接真正的类型编程魔法吧!