TS接口与对象类型
# 02.TS接口与对象类型
interface 如何描述"形状"?type 与 interface 的关键差异、索引签名、接口合并——构建可复用的类型契约。
# 1. 案例引入
# 1.1 被 API 变更背刺的教训
// 后端 API 返回的用户对象
function renderUser(user: { name: string; age: number }) {
return `${user.name}, ${user.age}`;
}
// 后来 API 增加了 role 字段,又删了 age……
renderUser({ name: "张三", age: 25, role: "admin" });
// ❌ 对象字面量直接传入时,TS 会进行"多余属性检查"
// 再过一个月,后端的结构变成:
// { userName, userAge, userRole }
// 前端所有函数签名都要改——噩梦开始了
# 1.2 interface 的初衷:一次定义,处处复用
interface User {
name: string;
age: number;
}
function renderUser(user: User): string {
return `${user.name}, ${user.age}`;
}
function updateUser(user: User): void { /* ... */ }
function deleteUser(id: string): void { /* ... */ }
// 当 API 变更时,只需修改一处 interface 定义
// 所有使用该类型的函数立刻获得类型提示
这就是 interface 的核心价值——描述对象的"形状",并成为代码中可复用的类型契约。
# 2. interface 语法全解
# 2.1 对象类型的基础声明
interface Person {
name: string;
age: number;
// 可选属性
email?: string;
// 只读属性
readonly id: number;
}
const p: Person = {
name: "张三",
age: 25,
id: 1
};
// p.id = 2; // ❌ 只读属性不可修改
# 2.2 interface 可以描述函数类型
// 等价于 type MyFn = (a: number, b: number) => number;
interface MyFn {
(a: number, b: number): number;
}
const add: MyFn = (a, b) => a + b;
// a 和 b 自动推断为 number,不必重复标注
# 2.3 interface 可以描述可索引类型
// 数字索引——类似数组
interface NumberArray {
[index: number]: string;
}
const arr: NumberArray = ["a", "b", "c"];
arr[0]; // string
// 字符串索引——类似字典
interface StringDictionary {
[key: string]: number;
}
const dict: StringDictionary = { "apple": 1, "banana": 2 };
dict["apple"]; // number
# 2.4 索引签名的类型兼容规则
// 数字索引的返回值必须是字符串索引返回值的子类型
interface Okay {
[index: number]: number; // 数字索引
[key: string]: number; // 字符串索引(和数字索引同类型,OK)
}
interface NotOkay {
[index: number]: string; // 数字索引 → string
// [key: string]: number; // ❌ 字符串索引返回值(number) 不是 string 的父类型
}
// 原因:JS 中 obj[0] 等价于 obj["0"],所以数字索引的类型必须兼容字符串索引
# 3. type 与 interface:七组关键差异
# 差异 1:声明合并
// interface 同名自动合并(Declaration Merging)
interface User {
name: string;
}
interface User {
age: number;
}
// 最终 User = { name: string; age: number }
// type 同名报错
// type User = { email: string }; // ❌ 重复标识符
# 差异 2:扩展方式
// interface 用 extends
interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }
// type 用 &
type Cat = Animal & { meow(): void; };
// extends 会做更多检查:如果有属性冲突会报错
// & 遇到冲突属性可能产生 never
# 差异 3:原始类型的别名
// type 可以为基本类型创建别名
type ID = string;
type Name = string;
// interface 只能描述对象结构
// interface Wrong = string; // ❌
# 差异 4:联合类型与元组
// type 支持联合和元组
type Status = "active" | "inactive";
type Point = [number, number];
// interface 不支持
# 差异 5:映射类型
// type 配合 keyof 做映射(第 05 篇详解)
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
// interface 做不了这种动态类型推导
# 差异 6:implements 与 extends
// 类和 interface 配合更自然
interface Serializable {
serialize(): string;
}
// class 用 implements 实现 interface
class UserModel implements Serializable {
name!: string;
serialize() { return JSON.stringify(this); }
}
// 类也可以用 extends 继承类,interface 也可以 extends 类
// 但 type 不能被 implements
# 差异 7:性能
// TypeScript 官方推荐:优先使用 interface
// 原因:interface 在编译器内部有缓存优化
// type 每次遇到都要重新计算交叉类型
// 差异在大型项目(>1000 类型)中可感
# 选型决策树
你要描述什么?
├── 对象/类的结构 → interface(可合并、可继承)
├── 联合类型 → type(interface 做不到)
├── 元组 → type
├── 映射类型 → type(配合 keyof)
├── 基本类型别名 → type
└── 对外暴露的公共 API → 优先 interface
# 4. 对象类型的进阶技巧
# 4.1 多余属性检查(Excess Property Checking)
interface Config {
url: string;
timeout?: number;
}
// 对象字面量——严格检查
const config1: Config = {
url: "https://api.com",
timeout: 5000,
// retries: 3 // ❌ 多余属性
};
// 中间变量——绕过检查
const config2 = {
url: "https://api.com",
timeout: 5000,
retries: 3 // 被允许(因为 config2 类型推断为 { url: string; timeout: number; retries: number })
};
const config3: Config = config2; // ✅ 结构兼容即可
要点:只有对象字面量直接赋值时才触发多余属性检查——这是为了防止打字错误,而不是严格的"密封"语义。
# 4.2 只读与属性的修饰符
interface Box {
readonly width: number;
readonly height: number;
// 可选属性的 getter 式写法
readonly area: number;
}
// readonly vs const 的选择:
// const → 变量声明
// readonly → 对象/interface 属性
# 4.3 函数属性 vs 方法声明
interface Handler {
// 方法声明
onClick(e: Event): void;
// 函数属性
onHover: (e: Event) => void;
}
// 关键差异:strictFunctionTypes 下
// 方法声明允许双向协变(更宽松),函数属性严格逆变(更安全)
// 推荐:日常开发用方法声明,库开发用函数属性
# 5. 索引签名的实战模式
# 5.1 动态键名访问
interface Dictionary<T> {
[key: string]: T;
}
const cache: Dictionary<number> = {};
cache["user_1"] = Date.now();
cache["user_2"] = Date.now();
console.log(cache["user_1"]); // number
# 5.2 已知属性 + 动态属性混合
interface Window {
title: string;
width: number;
// 同时允许任意字符串属性
[prop: string]: string | number;
}
const w: Window = {
title: "My App",
width: 1024,
customProp: "hello" // ✅
};
# 5.3 数字索引 vs 字符串索引
interface StringArray {
[index: number]: string; // 数字索引:返回 string
length: number; // 明确声明的属性
// [key: string]: number; // ❌ 与数字索引冲突——JS 下 arr[1] 等同于 arr["1"]
}
# 6. interface 的继承与合并
# 6.1 extends 单继承与多继承
interface Shape {
color: string;
}
interface Circle extends Shape {
radius: number;
}
interface Square extends Shape {
side: number;
}
// 多继承:圆角方形
interface RoundedSquare extends Circle, Square {
borderRadius: number;
}
// RoundedSquare 包含:color + radius + side + borderRadius
# 6.2 extends 与泛型约束(第 04 篇前奏)
// interface 泛型 + extends 约束
interface APIResponse<T> {
data: T;
status: number;
}
interface UserAPI extends APIResponse<User> {
// 自动拥有 data: User + status: number
}
# 6.3 声明合并(Declaration Merging)
// TS 内置类型就是用声明合并扩展的
interface Window {
title: string;
}
// 在另一个文件(或者同一全局作用域)
interface Window {
myCustomProp: number;
}
// 结果:Window = { title: string; myCustomProp: number }
// 实际应用:为 Vue 实例添加 $router 类型
// interface ComponentCustomProperties {
// $router: Router;
// }
注意:非导出的 interface 在全局合并;export interface 只在模块内有效,必须用 declare module 才能合并。
# 7. 实战案例:API 类型体系设计
# 7.1 设计可扩展的 API 响应类型
// 分三层设计:基础 → 通用 → 具体
// Layer 1:基础通用
interface BaseResponse {
code: number;
message: string;
}
// Layer 2:带数据的响应
interface DataResponse<T> extends BaseResponse {
data: T;
}
// Layer 3:带分页的响应
interface PaginatedResponse<T> extends DataResponse<T[]> {
pagination: {
page: number;
pageSize: number;
total: number;
};
}
// 使用时:
type UserListResponse = PaginatedResponse<User>;
// 自动包含:code/message/data: User[]/pagination
# 7.2 接口的多态设计
// 不同类型的通知,共享公共字段
interface Notification {
id: string;
createdAt: Date;
read: boolean;
}
interface MessageNotification extends Notification {
type: "message";
content: string;
senderId: string;
}
interface AlertNotification extends Notification {
type: "alert";
title: string;
priority: number;
}
// 联合类型收窄(第 06 篇详解)
type AppNotification = MessageNotification | AlertNotification;
function handle(n: AppNotification) {
if (n.type === "message") {
console.log(n.content); // n 收窄为 MessageNotification
}
}
# 8. 速查表
| 操作 | interface | type |
|---|---|---|
| 描述对象 | ✅ { a: number } | ✅ 同左 |
| 描述函数 | ✅ 调用签名 | ✅ (a:number)=>void |
| 描述联合 | ❌ | ✅ A \| B |
| 描述元组 | ❌ | ✅ [string,number] |
| 基本类型别名 | ❌ | ✅ type ID = string |
| 同名合并 | ✅ | ❌ 报错 |
| extends / implements | ✅ | ❌ |
| 映射类型 | ❌ | ✅ { [K in keyof T] } |
| 交叉类型 | ❌ (用 extends) | ✅ A & B |
| 编译器缓存 | ✅ 有缓存 | ⚠️ 每次计算 |
一句话选型:对象优先 interface,联合/元组/映射用 type;对外 API 用 interface 以便使用者扩展。
下一篇:03.TS函数与类实战
上次更新: 2026/06/24, 12:59:24