WinddSnow

TypeScript-Interfaces-and-Type-Aliases

字数统计: 3.3k阅读时长: 13 min
2026/07/30

第65课:接口与类型别名——interface vs type、声明合并、索引签名、只读属性

在 TypeScript 中,定义对象形状有两种核心方式:接口(interface类型别名(type。它们都能为对象、函数等复杂结构赋予明确的类型约束,但在语法细节、扩展方式和适用场景上存在重要差异。理解何时使用 interface、何时使用 type,是编写清晰、可维护的 TypeScript 代码的关键。本节课将深入两者的语法、扩展机制、声明合并特性,并讲解索引签名和只读属性的实战用法。


1. interface:对象形状的声明契约

interface 用于定义一个对象的形状——它应该包含哪些属性,每个属性的类型是什么。

1
2
3
4
5
6
7
8
9
10
11
interface User {
id: number;
name: string;
email: string;
}

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

1.1 可选属性

使用 ? 标记属性为可选的。可选属性在赋值时可以缺失,但在访问时需要处理 undefined 的情况。

1
2
3
4
5
6
7
interface Config {
url: string;
timeout?: number; // 可选
retries?: number; // 可选
}

const defaultConfig: Config = { url: 'https://api.example.com' };

1.2 只读属性

使用 readonly 修饰符标记属性在对象初始化后不可修改

1
2
3
4
5
6
7
interface Point {
readonly x: number;
readonly y: number;
}

const p: Point = { x: 10, y: 20 };
// p.x = 30; // ❌ 错误:无法分配到 "x",因为它是只读属性

readonly 仅阻止对属性本身的赋值。如果属性值是对象,对象内部的属性依然可以修改(浅层不可变)。

1
2
3
4
5
6
interface User {
readonly metadata: { createdAt: Date; updatedAt: Date };
}

const u: User = { metadata: { createdAt: new Date(), updatedAt: new Date() } };
u.metadata.updatedAt = new Date(); // ✅ 允许:修改嵌套对象属性

如需深层不可变,可以使用 readonly 数组、Readonly<T> 工具类型或递归的 DeepReadonly 类型。

1.3 函数签名

interface 可以描述函数类型(调用签名):

1
2
3
4
5
interface GreetFunction {
(name: string): string;
}

const greet: GreetFunction = (name) => `Hello, ${name}!`;

1.4 接口继承(extends

一个接口可以继承另一个接口的所有成员,并添加新成员。支持多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person {
name: string;
age: number;
}

interface Employee extends Person {
employeeId: number;
department: string;
}

const emp: Employee = {
name: 'Bob',
age: 30,
employeeId: 1001,
department: 'Engineering'
};

2. type 别名:更灵活的类型定义

type 关键字为任何类型创建一个别名。它不仅可以定义对象形状,还可以定义联合类型、交叉类型、元组、基本类型的别名等。

1
2
3
4
5
6
7
8
type Point = {
x: number;
y: number;
};

type ID = string | number; // 联合类型
type Coordinates = [number, number]; // 元组
type Callback = (data: string) => void; // 函数类型

2.1 typeinterface 的核心差异

对比维度 interface type
声明合并 支持。同一作用域多次声明同名 interface 自动合并。 不支持。重复声明同名 type 会报错。
扩展方式 使用 extends 继承其他 interface(或 type 的交叉类型)。 使用交叉类型 & 组合多个类型。
能定义的类型形式 仅能定义对象形状(包括函数签名和索引签名)。 可定义任意类型:联合类型、交叉类型、元组、基本类型别名、映射类型。
IDE 提示 编辑器通常直接显示 interface 名称,错误信息更友好。 复杂映射类型可能被编辑器展开为完整结构,可读性略差。
适用场景 定义公开 API、组件 Props、需要扩展或合并的数据模型。 定义工具类型、联合/交叉类型、临时别名、需要元组或映射的类型。

2.2 扩展类型别名

类型别名不能使用 extends,但可以通过交叉类型(& 组合多个类型。

1
2
3
4
5
6
7
8
9
type Person = {
name: string;
age: number;
};

type Employee = Person & {
employeeId: number;
department: string;
};

2.3 类型别名与接口的相互引用

两者可以互相引用:接口可以扩展类型别名(前提是该类型别名是对象类型),类型别名也可以交叉接口。

1
2
3
4
5
6
7
8
type Address = {
street: string;
city: string;
};

interface User extends Address {
name: string;
}

3. 声明合并(Declaration Merging)

这是 interface 独有的特性。如果多次定义同名的 interface,TypeScript 编译器会自动将它们合并为一个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Book {
title: string;
}

interface Book {
author: string;
pages: number;
}

// 合并后的 Book 接口:
// { title: string; author: string; pages: number; }

const book: Book = {
title: 'TypeScript Handbook',
author: 'Microsoft',
pages: 500
};

用途:声明合并在扩展第三方库的类型时极为有用。例如,给 Window 接口添加自定义属性,或给 expressRequest 添加用户信息。

1
2
3
4
5
6
7
8
// 全局扩展 Window 接口
declare global {
interface Window {
myAppConfig: { version: string };
}
}

// 现在可以直接使用 window.myAppConfig

注意事项

  • 只有 interface 支持声明合并,type 不支持。
  • 合并时,同名非函数成员的类型必须相同,否则编译器报错。
  • 同名函数成员会被视为函数重载(后声明的接口函数签名优先级更高)。

4. 索引签名(Index Signatures)

索引签名允许你描述一个对象可以通过任意字符串或数字键访问的值的类型。它适用于“字典”模式——键名未知但值类型已知的场景。

1
2
3
4
5
6
7
8
9
interface StringDictionary {
[key: string]: string; // 任何字符串键的值必须是 string
}

const dict: StringDictionary = {
hello: 'world',
foo: 'bar'
};
console.log(dict['hello']); // 'world'

4.1 数字索引签名

1
2
3
4
5
6
interface NumberArray {
[index: number]: string; // 数字索引对应的值必须是 string
}

const arr: NumberArray = ['a', 'b', 'c'];
console.log(arr[0]); // 'a'

注意:JavaScript 中,对象的键总是先转为字符串。obj[0] 实际等同于 obj['0']。因此,数字索引签名的返回值必须与字符串索引签名的返回值兼容。

1
2
3
4
interface Mixed {
[key: string]: number | string;
[index: number]: number; // ✅ number 是 number | string 的子类型
}

4.2 索引签名与固定属性

当对象同时拥有固定属性和索引签名时,固定属性的类型必须兼容索引签名的类型

1
2
3
4
5
interface UserDict {
[key: string]: string;
name: string; // ✅ 兼容 [key: string]: string
// age: number; // ❌ 错误:number 不能赋值给 string
}

如果需要同时包含固定属性和索引签名,可以将索引签名的值设为联合类型:

1
2
3
4
5
interface MixedDict {
[key: string]: string | number;
name: string;
age: number; // ✅ string | number 可以同时容纳 string 和 number
}

4.3 索引签名 vs 工具类型 Record

Record<K, V> 工具类型是定义字典类型的另一种方式。

1
2
type StringDict = Record<string, string>;
// 等价于 { [key: string]: string }

Record 更适合已知键名集合的场景(如 Record<'home' | 'about', PageInfo>)。索引签名更适合“键名完全未知”的通用字典。


5. 实战:接口设计原则

5.1 何时使用 interface

  • 需要定义对象的结构,且预期该结构可能被扩展或合并
  • 定义公共 API 的契约(如组件 Props、后端接口返回的数据形状)。
  • 需要利用声明合并扩展第三方库类型。

5.2 何时使用 type

  • 需要定义联合类型、交叉类型、元组
  • 需要创建函数签名别名复杂条件类型
  • 仅为临时性、不需要扩展的对象形状。

5.3 避免过度使用可选属性

大量可选属性可能意味着接口设计不够合理——可考虑拆分为更小、更专注的接口,或使用组合。


课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 以下关于 TypeScript 中 interfacetype 的描述,哪项是错误的?
    A. interface 可以通过 extends 继承另一个接口。
    B. type 可以使用交叉类型 & 组合多个类型。
    C. type 支持声明合并,可以多次定义同名 type。
    D. interface 可以定义函数签名。

  2. (单选) 定义一个对象,其所有字符串键的值必须为 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 }

  3. (填空) 要将一个接口的所有属性标记为只读,TypeScript 提供了内置工具类型 ______<T>(首字母大写)。

  4. (多选) 以下哪些是 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)、authorUser 接口)、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 类型定义代码。提示词中需明确指定 interfacetypePick、索引签名、Readonly 的使用方式。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建一个 TypeScript 类型定义文件 src/types/blog.ts,为博客系统设计完整的类型系统。需要包含:

  1. interface User { readonly id: number; name: string; email: string; role: 'admin' | 'editor' | 'viewer'; }
  2. interface Post { readonly id: number; title: string; content: string; author: User; tags: string[]; createdAt: Date; [key: string]: any; }(注意索引签名允许额外属性)
  3. type PostPreview = Pick<Post, 'id' | 'title' | 'tags' | 'createdAt'> & { authorName: string };
  4. 使用 Readonly<Post> 创建一个 ReadonlyPost 类型。
  5. 在文件末尾添加示例代码(使用 export {} 保持模块隔离):创建一个 User 对象和一个 Post 对象;创建一个 PostPreview 对象;尝试修改 ReadonlyPost 的属性并注释说明会报错。
  6. 所有导出添加 JSDoc 注释说明用途。使用严格 TypeScript 语法。完成后输出完整文件内容。

四、面试真题与参考答案

题目(字节跳动前端面试题):

请详细说明 TypeScript 中 interfacetype 的区别,并解释“声明合并”是什么。在什么场景下应该优先使用 interface,什么场景下优先使用 type

参考答案

1. 核心区别

对比维度 interface type
声明合并 支持。多次声明同名 interface 会合并成员。 不支持。重复声明同名 type 会报错。
扩展方式 extends 继承其他 interface(也可继承 type 的对象类型)。 使用交叉类型 & 组合多个类型。
可定义的形式 仅对象形状(包括函数签名、索引签名)。 任意类型:联合、交叉、元组、映射类型、基本类型别名等。
错误提示 通常显示 interface 名称,更清晰。 复杂映射可能被展开,可读性略差。

2. 声明合并:当在同一作用域中多次声明同名 interface 时,TypeScript 会将它们合并为一个接口,属性叠加,函数成员变为重载。这一特性常用于扩展第三方库的类型定义(如 WindowRequest)。type 不支持此特性。

3. 选择原则

  • **优先 interface**:定义对象的形状,尤其是公共 API、组件 Props、需要被扩展或合并的数据模型。
  • **优先 type**:定义联合类型、交叉类型、元组、函数签名别名、复杂条件类型或映射类型。当一个结构不需要被扩展时,type 也是完全可行的选择。

课后练习答案

一、概念自测答案

  1. C

    • 解析:type 不支持声明合并,同名 type 会报错。声明合并是 interface 独有的特性。
  2. B

    • 解析:[key: string]: number 定义字符串索引签名,所有字符串键的值必须是 number。A 是数字索引签名。
  3. Readonly

    • 解析:Readonly<T> 将类型 T 的所有属性变为只读。
  4. 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 注释。输出完整代码。”
CATALOG
  1. 1. 第65课:接口与类型别名——interface vs type、声明合并、索引签名、只读属性
    1. 1.1. 1. interface:对象形状的声明契约
      1. 1.1.1. 1.1 可选属性
      2. 1.1.2. 1.2 只读属性
      3. 1.1.3. 1.3 函数签名
      4. 1.1.4. 1.4 接口继承(extends)
    2. 1.2. 2. type 别名:更灵活的类型定义
      1. 1.2.1. 2.1 type 与 interface 的核心差异
      2. 1.2.2. 2.2 扩展类型别名
      3. 1.2.3. 2.3 类型别名与接口的相互引用
    3. 1.3. 3. 声明合并(Declaration Merging)
    4. 1.4. 4. 索引签名(Index Signatures)
      1. 1.4.1. 4.1 数字索引签名
      2. 1.4.2. 4.2 索引签名与固定属性
      3. 1.4.3. 4.3 索引签名 vs 工具类型 Record
    5. 1.5. 5. 实战:接口设计原则
      1. 1.5.1. 5.1 何时使用 interface
      2. 1.5.2. 5.2 何时使用 type
      3. 1.5.3. 5.3 避免过度使用可选属性
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)