对象与接口

用 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 只是编译时检查,运行时并不阻止修改。深层只读需要用 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 支持联合类型和元组。选哪个?我们用一张对比表把差异一次说清,给你一个明确的选择指南。

全面对比

大多数场景可以互换,少数场景各有所长

特性interfacetype
描述对象OKOK
继承/扩展extends交叉类型 &
声明合并支持不支持
描述联合类型不行OK type A = B | C
描述基本类型不行OK type ID = string
描述元组不行OK type Pair = [A, B]
implementsOKOK
错误信息更友好一般

建议:描述对象形状时优先用 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 少实现了一个方法,TS 会立刻报错: 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();
}
陷阱:多个接口有同名方法但返回类型不同时,类无法同时实现它们——TS 会报错。设计接口时避免这种冲突。

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 定义字典映射。别手写重复的接口!
图:工具类型对接口形状的变换

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 };
注意:递归类型必须至少有一个分支不是递归的(即"终止条件"),否则 TS 会报 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>。如果只有一个地方用到,直接写具体类型更清晰。泛型是为了复用,不是为了炫技。

结构化检查过程

为什么字面量赋值报错,变量赋值不报错?

图:结构化类型检查流程
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 被静默忽略
设计意图:字面量直接赋值时,TS 认为你是"精确描述",多余属性很可能是拼写错误。通过变量赋值时,TS 认为你只是"取子集",多余属性是合理的。

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 操作符辅助推断

下一章

接口掌握了,下一章学习联合类型、交叉类型与收窄——让类型系统更灵活、更安全。

1 / X