第65课:接口与类型别名——interface vs type、声明合并、索引签名、只读属性
在 TypeScript 中,定义对象形状有两种核心方式:接口(interface) 和类型别名(type)。它们都能为对象、函数等复杂结构赋予明确的类型约束,但在语法细节、扩展方式和适用场景上存在重要差异。理解何时使用 interface、何时使用 type,是编写清晰、可维护的 TypeScript 代码的关键。本节课将深入两者的语法、扩展机制、声明合并特性,并讲解索引签名和只读属性的实战用法。
1. interface:对象形状的声明契约
interface 用于定义一个对象的形状——它应该包含哪些属性,每个属性的类型是什么。
1 | interface User { |
1.1 可选属性
使用 ? 标记属性为可选的。可选属性在赋值时可以缺失,但在访问时需要处理 undefined 的情况。
1 | interface Config { |
1.2 只读属性
使用 readonly 修饰符标记属性在对象初始化后不可修改。
1 | interface Point { |
readonly 仅阻止对属性本身的赋值。如果属性值是对象,对象内部的属性依然可以修改(浅层不可变)。
1 | interface User { |
如需深层不可变,可以使用 readonly 数组、Readonly<T> 工具类型或递归的 DeepReadonly 类型。
1.3 函数签名
interface 可以描述函数类型(调用签名):
1 | interface GreetFunction { |
1.4 接口继承(extends)
一个接口可以继承另一个接口的所有成员,并添加新成员。支持多重继承。
1 | interface Person { |
2. type 别名:更灵活的类型定义
type 关键字为任何类型创建一个别名。它不仅可以定义对象形状,还可以定义联合类型、交叉类型、元组、基本类型的别名等。
1 | type Point = { |
2.1 type 与 interface 的核心差异
| 对比维度 | interface |
type |
|---|---|---|
| 声明合并 | 支持。同一作用域多次声明同名 interface 自动合并。 | 不支持。重复声明同名 type 会报错。 |
| 扩展方式 | 使用 extends 继承其他 interface(或 type 的交叉类型)。 |
使用交叉类型 & 组合多个类型。 |
| 能定义的类型形式 | 仅能定义对象形状(包括函数签名和索引签名)。 | 可定义任意类型:联合类型、交叉类型、元组、基本类型别名、映射类型。 |
| IDE 提示 | 编辑器通常直接显示 interface 名称,错误信息更友好。 | 复杂映射类型可能被编辑器展开为完整结构,可读性略差。 |
| 适用场景 | 定义公开 API、组件 Props、需要扩展或合并的数据模型。 | 定义工具类型、联合/交叉类型、临时别名、需要元组或映射的类型。 |
2.2 扩展类型别名
类型别名不能使用 extends,但可以通过交叉类型(&) 组合多个类型。
1 | type Person = { |
2.3 类型别名与接口的相互引用
两者可以互相引用:接口可以扩展类型别名(前提是该类型别名是对象类型),类型别名也可以交叉接口。
1 | type Address = { |
3. 声明合并(Declaration Merging)
这是 interface 独有的特性。如果多次定义同名的 interface,TypeScript 编译器会自动将它们合并为一个接口。
1 | interface Book { |
用途:声明合并在扩展第三方库的类型时极为有用。例如,给 Window 接口添加自定义属性,或给 express 的 Request 添加用户信息。
1 | // 全局扩展 Window 接口 |
注意事项:
- 只有
interface支持声明合并,type不支持。 - 合并时,同名非函数成员的类型必须相同,否则编译器报错。
- 同名函数成员会被视为函数重载(后声明的接口函数签名优先级更高)。
4. 索引签名(Index Signatures)
索引签名允许你描述一个对象可以通过任意字符串或数字键访问的值的类型。它适用于“字典”模式——键名未知但值类型已知的场景。
1 | interface StringDictionary { |
4.1 数字索引签名
1 | interface NumberArray { |
注意:JavaScript 中,对象的键总是先转为字符串。obj[0] 实际等同于 obj['0']。因此,数字索引签名的返回值必须与字符串索引签名的返回值兼容。
1 | interface Mixed { |
4.2 索引签名与固定属性
当对象同时拥有固定属性和索引签名时,固定属性的类型必须兼容索引签名的类型。
1 | interface UserDict { |
如果需要同时包含固定属性和索引签名,可以将索引签名的值设为联合类型:
1 | interface MixedDict { |
4.3 索引签名 vs 工具类型 Record
Record<K, V> 工具类型是定义字典类型的另一种方式。
1 | type StringDict = Record<string, string>; |
Record 更适合已知键名集合的场景(如 Record<'home' | 'about', PageInfo>)。索引签名更适合“键名完全未知”的通用字典。
5. 实战:接口设计原则
5.1 何时使用 interface
- 需要定义对象的结构,且预期该结构可能被扩展或合并。
- 定义公共 API 的契约(如组件 Props、后端接口返回的数据形状)。
- 需要利用声明合并扩展第三方库类型。
5.2 何时使用 type
- 需要定义联合类型、交叉类型、元组。
- 需要创建函数签名别名或复杂条件类型。
- 仅为临时性、不需要扩展的对象形状。
5.3 避免过度使用可选属性
大量可选属性可能意味着接口设计不够合理——可考虑拆分为更小、更专注的接口,或使用组合。
课后练习
一、概念自测(选择题 / 填空题)
(单选) 以下关于 TypeScript 中
interface和type的描述,哪项是错误的?
A.interface可以通过extends继承另一个接口。
B.type可以使用交叉类型&组合多个类型。
C.type支持声明合并,可以多次定义同名 type。
D.interface可以定义函数签名。(单选) 定义一个对象,其所有字符串键的值必须为
number,应使用?
A.interface Obj { [index: number]: number }
B.interface Obj { [key: string]: number }
C.interface Obj { key: number }
D.interface Obj { readonly [key: string]: number }(填空) 要将一个接口的所有属性标记为只读,TypeScript 提供了内置工具类型
______<T>(首字母大写)。(多选) 以下哪些是
type可以定义但interface无法直接定义的类型形式?
A. 联合类型string | number
B. 元组类型[string, number]
C. 对象形状{ name: string }
D. 基本类型的别名type Age = number
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要为一个博客系统设计 TypeScript 类型。要求如下:
- 使用
interface定义Post类型,包含id(只读 number)、title(string)、content(string)、author(User接口)、tags(string 数组)、createdAt(Date)。 - 使用
interface定义User类型,包含id(只读 number)、name(string)、email(string)、role(联合类型:'admin' | 'editor' | 'viewer')。 - 使用
type定义PostPreview类型,它是Pick<Post, 'id' | 'title' | 'tags' | 'createdAt'>加上一个authorName: string属性(即从 User 中提取name字段)。 - 为
Post添加索引签名[key: string]: any,允许任意额外属性(如metadata)。 - 使用
Readonly<T>工具类型创建ReadonlyPost,并演示对一个已有的 Post 对象进行只读保护。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 TypeScript 类型定义代码。提示词中需明确指定 interface、type、Pick、索引签名、Readonly 的使用方式。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请创建一个 TypeScript 类型定义文件
src/types/blog.ts,为博客系统设计完整的类型系统。需要包含:
interface User { readonly id: number; name: string; email: string; role: 'admin' | 'editor' | 'viewer'; }interface Post { readonly id: number; title: string; content: string; author: User; tags: string[]; createdAt: Date; [key: string]: any; }(注意索引签名允许额外属性)type PostPreview = Pick<Post, 'id' | 'title' | 'tags' | 'createdAt'> & { authorName: string };- 使用
Readonly<Post>创建一个ReadonlyPost类型。- 在文件末尾添加示例代码(使用
export {}保持模块隔离):创建一个User对象和一个Post对象;创建一个PostPreview对象;尝试修改ReadonlyPost的属性并注释说明会报错。- 所有导出添加 JSDoc 注释说明用途。使用严格 TypeScript 语法。完成后输出完整文件内容。
四、面试真题与参考答案
题目(字节跳动前端面试题):
请详细说明 TypeScript 中
interface和type的区别,并解释“声明合并”是什么。在什么场景下应该优先使用interface,什么场景下优先使用type?
参考答案:
1. 核心区别:
| 对比维度 | interface |
type |
|---|---|---|
| 声明合并 | 支持。多次声明同名 interface 会合并成员。 | 不支持。重复声明同名 type 会报错。 |
| 扩展方式 | extends 继承其他 interface(也可继承 type 的对象类型)。 |
使用交叉类型 & 组合多个类型。 |
| 可定义的形式 | 仅对象形状(包括函数签名、索引签名)。 | 任意类型:联合、交叉、元组、映射类型、基本类型别名等。 |
| 错误提示 | 通常显示 interface 名称,更清晰。 | 复杂映射可能被展开,可读性略差。 |
2. 声明合并:当在同一作用域中多次声明同名 interface 时,TypeScript 会将它们合并为一个接口,属性叠加,函数成员变为重载。这一特性常用于扩展第三方库的类型定义(如 Window、Request)。type 不支持此特性。
3. 选择原则:
- **优先
interface**:定义对象的形状,尤其是公共 API、组件 Props、需要被扩展或合并的数据模型。 - **优先
type**:定义联合类型、交叉类型、元组、函数签名别名、复杂条件类型或映射类型。当一个结构不需要被扩展时,type也是完全可行的选择。
课后练习答案
一、概念自测答案
C
- 解析:
type不支持声明合并,同名type会报错。声明合并是interface独有的特性。
- 解析:
B
- 解析:
[key: string]: number定义字符串索引签名,所有字符串键的值必须是number。A 是数字索引签名。
- 解析:
Readonly- 解析:
Readonly<T>将类型T的所有属性变为只读。
- 解析:
A、B、D
- 解析:联合类型、元组、基本类型别名只有
type可以直接定义。对象形状两者都可以定义。
- 解析:联合类型、元组、基本类型别名只有
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请生成一个 TypeScript 类型定义文件,包含博客系统的类型。要求:
interface User包含readonly id: number; name: string; email: string; role: 'admin' | 'editor' | 'viewer';interface Post包含readonly id: number; title: string; content: string; author: User; tags: string[]; createdAt: Date; [key: string]: any;type PostPreview = Pick<Post, 'id' | 'title' | 'tags' | 'createdAt'> & { authorName: string };- 用
Readonly<Post>定义一个ReadonlyPost类型。- 添加简短的 JSDoc 注释。输出完整代码。”