对象与接口
用 interface 描述对象的形状,让数据结构不再靠猜
🎯 学习目标
学完本章,你将能够:
- 能够使用 interface 定义对象形状,包括可选属性、只读属性和索引签名
- 能够运用 extends 实现接口继承与 implements 实现类约束
- 能够理解结构化类型检查(鸭式类型)与类型兼容性规则
- 能够使用 keyof、映射类型和内置工具类型操作对象类型
- 能够遵循接口设计原则编写可维护的类型定义
对象的问题
JS 里的对象就像一个黑箱,你不知道它有哪些属性
// JS:user 有哪些属性?不知道,全靠猜
function printUser(user) {
console.log(user.name); // name 存在吗?
console.log(user.age); // age 是数字还是字符串?
console.log(user.email); // email 必填吗?
}
// TS:用 interface 明确定义对象的形状
interface User {
name: string;
age: number;
email: string;
}
function printUser(user: User): void {
console.log(user.name); // TS 保证一定存在
console.log(user.age); // TS 保证是 number
}
interface 就是对象的"蓝图",定义了它应该有哪些属性、属性是什么类型。
Part 1: interface 语法
interface 只存在于编译时,不会产生任何运行时代码
前面我们用 interface 描述了一个 User 对象。但你有没有想过,编译之后这些 interface 去哪了?答案是——消失了。interface 纯粹是编译时的约束,帮你在写代码时挡住错误。接下来我们看看它最基本的用法:定义必填属性、可选属性、只读属性。
定义对象形状
属性数量必须完全匹配
// 基本接口
interface User {
name: string;
age: number;
email: string;
}
const user: User = {
name: "张三",
age: 25,
email: "zhangsan@example.com",
};
// 属性少了——报错
// const bad: User = { name: "张三" };
// 报错:缺少 age、email 属性
// 属性多了——也报错
// const extra: User = { name: "张三", age: 25, email: "a@b.c", phone: "123" };
// 报错:"phone" 不存在
可选属性
用 ? 标记,可以不提供
interface Config {
host: string;
port: number;
timeout?: number; // 可选,可以不提供
retry?: number; // 可选
}
const c1: Config = { host: "localhost", port: 3000 }; // OK
const c2: Config = { host: "localhost", port: 3000, timeout: 5000 }; // OK
用人话说:可选属性就像表格里的"选填项"——你不填也能提交,但填了就要符合格式。
只读属性
用 readonly 标记,创建后不可修改
interface Point {
readonly x: number;
readonly y: number;
}
const p: Point = { x: 10, y: 20 };
// p.x = 30; // 报错:无法分配到 "x",因为它是只读属性
Readonly<T> 泛型工具类型。
Part 2: 属性修饰符
索引签名、多种方法写法——让接口更灵活
必填、可选、只读——这三个修饰符能解决大部分场景。但现实开发中还有一种情况:属性名事先不确定。比如配置对象的键可能是任意字符串,怎么描述?索引签名就是为此而生。另外接口里定义方法也有好几种写法,我们一并搞清楚。
索引签名
当属性名不固定时使用
// 任意字符串键的值都是 string
interface Dictionary {
[key: string]: string;
}
const dict: Dictionary = {
hello: "你好",
world: "世界",
goodbye: "再见",
};
// 常见用法:配置对象
interface EnvConfig {
[envName: string]: string | number | boolean;
}
const env: EnvConfig = {
NODE_ENV: "production",
PORT: 3000,
DEBUG: false,
};
方法多种写法
接口中定义函数的几种方式
interface User {
readonly id: number; // 只读属性
nickname?: string; // 可选属性
name: string; // 普通属性
// 方法写法1:方法签名
greet(): string;
// 方法写法2:属性 + 函数类型
greet2: () => string;
// 方法写法3:简写
say(msg: string): void;
}
Part 3: 继承与合并
接口可以继承其他接口,同名的接口会自动合并
接口不会凭空出现——它们通常是层层组合出来的。一个 Animal 接口提供基础字段,Dog 在它之上加 breed。这和 class 的 extends 很像,但接口还多了一招:声明合并。两个同名的 interface 会自动合体,这正是给第三方库扩展类型的秘密武器。
extends 继承
单继承和多继承都支持
// 单继承
interface Animal {
name: string;
age: number;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
const dog: Dog = {
name: "旺财",
age: 3,
breed: "柴犬",
bark() { console.log("汪汪!"); },
};
// 多继承
interface Serializable {
serialize(): string;
}
interface Loggable {
log(): void;
}
interface Entity extends Serializable, Loggable {
id: number;
}
声明合并
同名 interface 自动合并属性
// 同名 interface 自动合并属性
interface Window {
title: string;
}
interface Window {
width: number;
height: number;
}
// 等同于
// interface Window {
// title: string;
// width: number;
// height: number;
// }
// 实际应用:扩展第三方库的类型
declare module "express" {
interface Request {
userId?: string; // 添加自定义属性
sessionId?: string;
}
}
interface Box { size: number; } 和 interface Box { size: string; } 会冲突。
Part 4: interface vs type
两者都能描述对象形状,但有细微区别
学到这里你可能会问:interface 能做的事,type 是不是也能做?确实,大多数情况下它们可以互换。但仔细对比会发现,interface 支持声明合并,type 支持联合类型和元组。选哪个?我们用一张对比表把差异一次说清,给你一个明确的选择指南。
全面对比
大多数场景可以互换,少数场景各有所长
| 特性 | interface | type |
|---|---|---|
| 描述对象 | OK | OK |
| 继承/扩展 | extends | 交叉类型 & |
| 声明合并 | 支持 | 不支持 |
| 描述联合类型 | 不行 | OK type A = B | C |
| 描述基本类型 | 不行 | OK type ID = string |
| 描述元组 | 不行 | OK type Pair = [A, B] |
| implements | OK | OK |
| 错误信息 | 更友好 | 一般 |
建议:描述对象形状时优先用 interface(可扩展、错误信息友好);描述联合类型、元组、工具类型时用 type。
Part 5: 类实现接口
interface 不仅能约束对象字面量,还能约束 class 的行为
前面四节都在用 interface 约束对象字面量。但 interface 的能力不止于此——它还能跟 class 签合同。用 implements 关键字,让类承诺"我会实现这些方法"。这在依赖注入和策略模式中几乎是标配。接下来看具体怎么用。
implements 基础
让类承诺"我会实现这些方法"
interface Logger {
log(message: string): void;
error(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
error(message: string) {
console.error(`[ERR] ${message}`);
}
}
const logger: Logger = new ConsoleLogger();
logger.log("启动完成"); // [LOG] 启动完成
implements 就像签合同:类声明它实现了某个接口,TS 会逐项检查——漏了一个方法就报错。
多接口与依赖注入
一个类可以实现多个接口,实现灵活的依赖注入
interface Logger {
log(message: string): void;
error(message: string): void;
}
interface FileLogger extends Logger {
getFilePath(): string;
}
class FileLogger implements FileLogger {
constructor(private filename: string) {}
log(message: string) { /* 写入文件 */ }
error(message: string) { /* 写入文件 */ }
getFilePath() { return this.filename; }
}
// 依赖注入:函数不关心具体实现,只关心接口
function processOrder(logger: Logger) {
logger.log("订单处理开始");
// ... 业务逻辑
logger.log("订单处理完成");
}
processOrder(new ConsoleLogger()); // OK
processOrder(new FileLogger("app.log")); // OK
class Bad implements Logger {} → 报错:缺少 log 和 error 方法。
implements 进阶
用接口实现策略模式,运行时灵活切换实现
// 策略模式:定义统一接口,多种实现可互换
interface StorageService {
get(key: string): string | null;
set(key: string, value: string): void;
remove(key: string): void;
}
class LocalStorageService implements StorageService {
get(key: string) { return localStorage.getItem(key); }
set(key: string, v: string) { localStorage.setItem(key, v); }
remove(key: string) { localStorage.removeItem(key); }
}
class MemoryStorageService implements StorageService {
private store = new Map<string, string>();
get(key: string) { return this.store.get(key) ?? null; }
set(key: string, v: string) { this.store.set(key, v); }
remove(key: string) { this.store.delete(key); }
}
// 工厂函数:根据环境选择实现
function createStorage(): StorageService {
return typeof localStorage !== "undefined"
? new LocalStorageService()
: new MemoryStorageService();
}
Part 6: 结构化检查
只要形状匹配就能赋值,不关心名称
你有没有遇到过这种困惑:明明没有写 extends,两个接口的变量却能互相赋值?这就是 TypeScript 的结构化类型系统在起作用。它只看"形状"是否兼容,不管"名字"叫什么。但有一个陷阱——字面量直接赋值和通过变量中转,检查的严格程度不一样。
鸭式类型检查
只要长得像,就是那个类型
// 结构化类型——鸭式类型检查
interface Point { x: number; y: number; }
const p: Point = { x: 10, y: 20 }; // OK
// const p2: Point = { x: 10, y: 20, z: 30 }; // 报错:多余属性
// 但通过变量中转就允许
const p3 = { x: 10, y: 20, z: 30 };
const p4: Point = p3; // OK!通过变量赋值时允许多余属性
// 函数参数的类似行为
function printPoint(p: Point) { console.log(p); }
printPoint({ x: 1, y: 2 }); // OK
// printPoint({ x: 1, y: 2, z: 3 }); // 报错
const obj = { x: 1, y: 2, z: 3 };
printPoint(obj); // OK
对象字面量会触发严格字面量检查,不允许多余属性。但通过变量中转就不会。
类型兼容性
源类型必须包含目标类型的所有属性
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
let animal: Animal = { name: "动物" };
let dog: Dog = { name: "旺财", breed: "柴犬" };
animal = dog; // OK:Dog 有 name
// dog = animal; // 报错:Animal 缺少 breed
// 函数参数兼容性
function feed(animal: Animal) { /* ... */ }
feed(dog); // OK:传入属性更多的对象
feed(animal); // OK
Part 7: 映射类型预览
用 keyof 和 in 操作符批量转换接口的属性
假设你有一个 User 接口,现在需要一个"所有属性都可选"的版本——手写一遍?太累了。keyof 能提取属性名,映射类型能批量变换。这两个工具组合起来,就能让 TypeScript 自动帮你生成新接口。后续章节会深入讲解,这里先混个脸熟。
keyof 操作符
获取接口的所有属性名
interface User {
name: string;
age: number;
email: string;
}
type UserKeys = keyof User; // "name" | "age" | "email"
// 安全访问属性
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { name: "张三", age: 25, email: "a@b.c" };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age"); // number
// getProperty(user, "phone"); // 报错!"phone" 不是 User 的属性
映射类型
批量转换属性的可选性和只读性
interface User {
name: string;
age: number;
email: string;
}
// Partial —— 所有属性变可选
type PartialUser = {
[K in keyof User]?: User[K];
};
// 等同于 { name?: string; age?: number; email?: string }
// Required —— 所有属性变必选
type RequiredUser = {
[K in keyof User]-?: User[K];
};
// Readonly —— 所有属性变只读
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
内置工具类型
TS 内置了一批常用映射类型,不用自己手写
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
// Partial<T> —— 所有属性变可选
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; role?: "admin" | "user" }
// Required<T> —— 所有属性变必选
type RequiredUser = Required<PartialUser>;
// 回到原始 User 的样子
// Readonly<T> —— 所有属性变只读
type FrozenUser = Readonly<User>;
// { readonly id: number; readonly name: string; ... }
这些工具类型本质上就是上一节学的映射类型的语法糖。TS 把最常用的几个内置了,第30章会深入讲解更多。
工具类型实战
CRUD 场景中 Partial / Required / Readonly 的真实用法
interface UserProfile {
id: number;
name: string;
email: string;
avatar: string;
}
// Partial —— PATCH 请求:只传要改的字段
function patchUser(id: number, data: Partial<UserProfile>) {
return fetch(`/users/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
patchUser(1, { name: "新名字" }); // OK,其他字段不改
patchUser(2, { email: "new@x.com" }); // OK
// Required —— 表单验证:确保用户填完所有必填项
function validateProfile(data: Partial<UserProfile>): Required<UserProfile> {
if (!data.name || !data.email) throw new Error("缺少必填字段");
return { id: data.id!, name: data.name, email: data.email, avatar: data.avatar! };
}
// Readonly —— 配置对象:启动后不允许修改
const config: Readonly<UserProfile> = { id: 0, name: "app", email: "a@b.c", avatar: "" };
// config.name = "changed"; // 报错:只读
Readonly<T> 只做浅层只读,嵌套对象仍可修改。深层只读需要自定义 DeepReadonly(后面会讲)。
Pick/Omit/Record
提取、排除、构造——最实用的三个工具类型
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
// Pick<T, K> —— 只保留指定属性
type PublicUser = Pick<User, "id" | "name">;
// { id: number; name: string }
// Omit<T, K> —— 排除指定属性
type CreateUserDTO = Omit<User, "id">;
// { name: string; email: string; role: "admin" | "user" }
// Record<K, V> —— 构造键值对类型
type UserMap = Record<number, User>;
// 等同于 { [key: number]: User }
const users: UserMap = {
1: { id: 1, name: "张三", email: "a@b.c", role: "admin" },
2: { id: 2, name: "李四", email: "d@e.f", role: "user" },
};
Omit 定义创建 DTO(去掉 id),用 Pick 定义公开视图(只暴露安全字段),用 Record 定义字典映射。别手写重复的接口!

图:工具类型对接口形状的变换
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 左侧:User接口卡片(id,name,email,role) → 右侧四种变换 - Partial→所有属性虚线边框"可选" | Pick<"id"|"name">→只保留这两个 | Omit<"email">→email划掉 | Record→键值对集合 - 中间箭头标注操作名 - 配色:浅灰背景,原始深蓝边框,变换后不同暖色,箭头橙色,16:9
Pick/Omit 实战
从一个 User 接口派生整套 CRUD DTO
interface User {
id: number;
name: string;
email: string;
password: string;
role: "admin" | "user";
createdAt: string;
}
// 创建:不需要 id 和 createdAt(由后端生成)
type CreateUserDTO = Omit<User, "id" | "createdAt">;
// { name, email, password, role }
// 更新:所有业务字段可选,但 id 必传
type UpdateUserDTO = Partial<Omit<User, "id" | "createdAt">> & { id: number };
// { id: number; name?: string; email?: string; ... }
// 公开视图:只暴露安全字段,去掉密码
type UserVO = Pick<User, "id" | "name" | "email" | "role">;
// Record —— 状态码映射
type HttpStatusText = Record<200 | 404 | 500, string>;
const statusMap: HttpStatusText = {
200: "OK",
404: "Not Found",
500: "Internal Server Error",
};
DRY 原则:所有 DTO 从同一个 User 派生,改 User 时所有 DTO 自动同步。再也不怕忘了改 DTO。
递归类型简介
接口可以引用自身——描述树形结构
// 树节点:children 又是 TreeNode 数组
interface TreeNode {
name: string;
children?: TreeNode[];
}
const tree: TreeNode = {
name: "根目录",
children: [
{ name: "src", children: [
{ name: "index.ts" },
{ name: "utils.ts" },
]},
{ name: "package.json" },
],
};
// JSON 类型也是递归的
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
Type produces a tuple type that is too large to represent 错误。另外,类型递归深度有限制,过深的嵌套会报错。
递归类型进阶
DeepPartial 与 DeepReadonly——递归工具类型
// DeepPartial:嵌套对象的所有层级都变可选
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Company {
name: string;
address: { city: string; district: string };
}
type PartialCompany = DeepPartial<Company>;
// { name?: string; address?: { city?: string; district?: string } }
// DeepReadonly:嵌套对象的所有层级都变只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
const config: DeepReadonly<Company> = {
name: "Acme",
address: { city: "北京", district: "海淀" },
};
// config.address.city = "上海"; // 报错!深层也只读
Partial<T> / Readonly<T>,只在确实需要深层操作时才用递归版本。
接口设计原则
写接口的几条实用建议
// 原则1:小而专注(接口隔离原则 ISP)
// 好的做法——按职责拆分
interface Printable { print(): void; }
interface Savable { save(): void; }
interface Loadable { load(): void; }
// 原则2:只读保护不该变的数据
interface Config {
readonly appName: string;
readonly version: string;
maxRetries: number; // 只有这个允许运行时修改
}
// 原则3:用 Pick/Omit 替代手写重复接口
interface User {
id: number; name: string; email: string; password: string;
}
type PublicUser = Pick<User, "id" | "name">; // 而不是重新定义
type CreateUserDTO = Omit<User, "id">; // 而不是重新定义
命名约定:社区有两种流派——IUser(加 I 前缀,C# 风格)和 User(不加前缀,TS 官方推荐)。推荐后者,因为 TS 的接口不只是类的契约,用 I 前缀反而显得冗余。关键是名字要描述清楚用途,如 CreateUserDTO、ApiResponse。
接口命名约定
DTO / VO / Entity 后缀与文件组织
// 按用途加后缀——一看名字就知道角色
interface User { ... } // 领域实体
interface CreateUserDTO { ... } // 创建请求 body
interface UpdateUserDTO { ... } // 更新请求 body
interface UserVO { ... } // 返回给前端的视图
interface UserRepository { ... } // 数据访问层接口
// 推荐:types/ 目录按领域组织
// types/user.ts —— 导出所有 User 相关类型
// types/api.ts —— 通用 ApiResponse、PaginatedResponse
// types/env.ts —— 环境变量类型声明
// user.ts 中统一导出,避免到处 import
export interface User { ... }
export type CreateUserDTO = Omit<User, "id">;
export type UserVO = Pick<User, "id" | "name">;
团队约定 > 个人偏好:不管选哪种风格,团队统一比"选对"更重要。用 ESLint 的 naming-convention 规则自动检查。
API 类型实战
用接口定义 Request/Response,前后端共享类型契约
// DTO 模式:Data Transfer Object
interface CreateUserRequest {
name: string;
email: string;
password: string;
}
interface CreateUserResponse {
id: string;
name: string;
email: string;
createdAt: string;
}
// 后端路由
app.post("/users", (req: Request<{}, CreateUserResponse, CreateUserRequest>, res) => {
const { name, email, password } = req.body; // 类型安全
// ...
});
// 前端调用——共享同一个接口
async function createUser(data: CreateUserRequest): Promise<CreateUserResponse> {
const res = await fetch("/users", {
method: "POST",
body: JSON.stringify(data),
});
return res.json();
}
好处:后端改了字段名,前端立即报错。接口就是前后端的"合同",谁改谁负责。
REST 泛型系统
ApiResponse<T> 与 PaginatedResponse<T> 通用封装
// 通用响应封装
interface ApiResponse<T> {
code: number;
message: string;
data: T;
timestamp: number;
}
// 分页响应——data 里带列表和分页信息
interface PaginatedResponse<T> {
code: number;
message: string;
data: {
list: T[];
total: number;
page: number;
pageSize: number;
hasNext: boolean;
};
timestamp: number;
}
// 实际使用:一个接口贯穿前后端
type UserResponse = ApiResponse<User>;
type UserListResponse = PaginatedResponse<User>;
// 前端请求函数
async function fetchUsers(page: number): Promise<UserListResponse> {
const res = await fetch(`/api/users?page=${page}`);
return res.json();
// res.data.list → User[]
// res.data.total → 总条数
// res.data.hasNext → 是否有下一页
}
<T>。如果只有一个地方用到,直接写具体类型更清晰。泛型是为了复用,不是为了炫技。
结构化检查过程
为什么字面量赋值报错,变量赋值不报错?

图:结构化类型检查流程
【AI 生图提示词】请生成一张中文技术流程图,风格简洁、配色柔和、适合教学: - 画面主体:两条并行路径的对比 - 上方路径(报错):对象字面量 {x:1, y:2, z:3} → 直接赋值给接口变量 Point → 触发严格多余属性检查 → 红色叉号"报错:z 是多余属性" - 下方路径(通过):对象字面量 {x:1, y:2, z:3} → 先赋值给普通变量 obj → 再赋值给接口变量 → 绿色勾号"通过,z 被忽略" - 右侧标注:"字面量直接赋值 → 严格检查 | 变量间接赋值 → 宽松检查" - 配色建议:浅灰背景,报错路径红色系,通过路径绿色系 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
interface Point { x: number; y: number; }
// 路径1:字面量直接赋值 → 严格检查
// const p1: Point = { x: 1, y: 2, z: 3 }; // 报错!z 是多余的
// 路径2:通过变量中转 → 允许多余属性
const temp = { x: 1, y: 2, z: 3 };
const p2: Point = temp; // OK,z 被静默忽略
satisfies 操作符
TS 4.9+:既验证类型,又保留精确推断
// 问题:用类型注解会丢失精确类型
const colors: Record<string, string | string[]> = {
primary: "#007bff", // TS 认为是 string | string[]
danger: "#dc3545",
gradients: ["#ff6b6b", "#ee5a24"],
};
// colors.primary.toUpperCase() → 报错!TS 不确定是 string 还是 string[]
// 解决:用 satisfies 验证类型,但保留精确推断
const colors = {
primary: "#007bff",
danger: "#dc3545",
gradients: ["#ff6b6b", "#ee5a24"],
} satisfies Record<string, string | string[]>;
colors.primary.toUpperCase(); // OK!TS 知道是 string
colors.gradients.map(c => c); // OK!TS 知道是 string[]
satisfies = "我要检查这个值满足某个类型,但别把类型信息拓宽"。类型注解 : T 会拓宽类型,satisfies T 不会。
练习:API 类型系统
设计一套完整的 REST API 类型
需求:定义 User、CreateUserDTO、UpdateUserDTO、ApiResponse 四个接口。CreateUserDTO 缺少 id,UpdateUserDTO 所有字段可选,ApiResponse 用泛型包裹数据。
// 你的代码:
// interface User { ... }
// interface CreateUserDTO { ... }
// interface UpdateUserDTO { ... }
// interface ApiResponse<T> { ... }
参考答案
interface User {
id: string;
name: string;
email: string;
avatar?: string;
createdAt: string;
}
interface CreateUserDTO {
name: string;
email: string;
password: string;
avatar?: string;
}
interface UpdateUserDTO {
name?: string;
email?: string;
avatar?: string;
}
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 使用
type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<User[]>;
常见接口错误
问题一:对象文字可以只指定已知属性
原因:对象字面量包含接口中没有的属性。
修复:删除多余属性,或者在接口中添加该属性。
问题二:缺少属性
原因:接口要求的必填属性没有提供。
修复:补上缺少的属性,或者把它改成可选属性(加 ?)。
问题三:不能将类型"X"分配给类型"Y"
原因:属性值的类型和接口定义的类型不一致。
修复:检查赋的值的类型是否和接口一致。
试一试
练习一
任务:定义一个 Product 接口,包含 name(必填)、price(必填)、description(可选)、readonly id(只读)。
// 你的代码:
参考答案
interface Product {
readonly id: number;
name: string;
price: number;
description?: string;
}
const p: Product = {
id: 1,
name: "键盘",
price: 299,
};
// p.id = 2; // 报错:只读
练习二
任务:定义一个 ApiResponse 接口,用索引签名支持任意数量的数据字段。
// 你的代码:
参考答案
interface ApiResponse {
code: number;
message: string;
[key: string]: string | number | boolean;
}
const res: ApiResponse = {
code: 200,
message: "OK",
total: 100,
hasMore: true,
};
📝 要点总结
本章核心知识点
接口定义
可选属性 ?、只读 readonly、索引签名 [key: string]
继承与实现
extends 多继承、implements 类实现接口、声明合并
类型兼容
结构化类型(鸭式)、多余属性检查、fresh object
类型操作
keyof 获取键、映射类型遍历属性、Pick/Omit/Record
设计原则
接口隔离、最小暴露、satisfies 操作符辅助推断
下一章
接口掌握了,下一章学习联合类型、交叉类型与收窄——让类型系统更灵活、更安全。