TypeScript 接口的基本使用 —— 定义对象形状

本文献给:

已掌握 TypeScript 类型别名、联合类型、交叉类型等知识的开发者。本文将带你学习接口(interface)的核心用法,包括定义对象形状、可选属性、只读属性、多余属性检查,以及接口与类型别名的区别与选择。

你将学到:

  1. 接口的基本语法与使用场景
  2. 可选属性 ? 与只读属性 readonly
  3. 多余属性检查与绕过方法
  4. 接口与类型别名 type 的对比
  5. 接口声明的合并特性

目录

  • 一、接口的基本语法
    • [1.1 接口作为类型注解](#1.1 接口作为类型注解)
  • [二、可选属性(Optional Properties)](#二、可选属性(Optional Properties))
  • [三、只读属性(Readonly Properties)](#三、只读属性(Readonly Properties))
    • [3.1 只读数组](#3.1 只读数组)
  • [四、多余属性检查(Excess Property Checks)](#四、多余属性检查(Excess Property Checks))
    • [4.1 绕过多余属性检查的方法](#4.1 绕过多余属性检查的方法)
  • 五、接口与类型别名的对比
    • [5.1 声明合并](#5.1 声明合并)
    • [5.2 扩展方式对比](#5.2 扩展方式对比)
    • [5.3 选择建议](#5.3 选择建议)
  • 六、接口的函数类型和索引签名(简介)
    • [6.1 函数类型接口](#6.1 函数类型接口)
    • [6.2 索引签名](#6.2 索引签名)
  • 七、常见错误与注意事项
    • [7.1 忘记使用 `=` 定义接口](#7.1 忘记使用 = 定义接口)
    • [7.2 使用 `interface` 定义联合类型](#7.2 使用 interface 定义联合类型)
    • [7.3 只读属性与 `const` 混淆](#7.3 只读属性与 const 混淆)
    • [7.4 多余属性检查意外触发](#7.4 多余属性检查意外触发)
  • 八、综合示例
  • 九、小结

一、接口的基本语法

接口用于定义对象的结构形状(shape),描述对象应该有哪些属性以及属性的类型。

typescript 复制代码
interface User {
    id: number;
    name: string;
    email: string;
}

function register(user: User) {
    console.log(`${user.name} registered`);
}

const alice: User = { id: 1, name: "Alice", email: "alice@example.com" };
register(alice);

接口可以定义函数类型、索引签名等,但最常见的是描述对象。

1.1 接口作为类型注解

接口可以用于变量、函数参数、函数返回值等任何需要类型的地方。

typescript 复制代码
interface Point {
    x: number;
    y: number;
}

function getDistance(p1: Point, p2: Point): number {
    return Math.hypot(p1.x - p2.x, p1.y - p2.y);
}

const origin: Point = { x: 0, y: 0 };
const target: Point = { x: 3, y: 4 };
console.log(getDistance(origin, target));  // 5

二、可选属性(Optional Properties)

使用 ? 标记属性为可选,表示该属性可以存在也可以不存在。

typescript 复制代码
interface Person {
    name: string;
    age?: number;      // 可选
    email?: string;    // 可选
}

const p1: Person = { name: "Bob" };              // OK
const p2: Person = { name: "Alice", age: 25 };   // OK

可选属性的类型实际上是 T | undefined

typescript 复制代码
function greet(person: Person) {
    console.log(`Hello, ${person.name}`);
    if (person.age !== undefined) {
        console.log(`Age: ${person.age}`);
    }
}

三、只读属性(Readonly Properties)

使用 readonly 关键字标记属性为只读,在对象创建后不可修改。

typescript 复制代码
interface Config {
    readonly apiUrl: string;
    readonly timeout: number;
    retries: number;   // 可修改
}

const config: Config = {
    apiUrl: "https://api.example.com",
    timeout: 5000,
    retries: 3
};

config.retries = 5;     // OK
config.apiUrl = "xxx";  // ❌ 只读属性不可修改

readonly 只在编译时检查,不影响运行时。对象内容本身是否可以修改取决于属性是否是只读。

3.1 只读数组

TypeScript 提供了 ReadonlyArray<T> 类型,以及 readonly 修饰符用于数组和元组。

typescript 复制代码
interface DataContainer {
    readonly items: number[];
    data: readonly string[];   // 只读数组
}

const container: DataContainer = {
    items: [1, 2, 3],
    data: ["a", "b"]
};

container.items.push(4);   // ❌ 只读属性,不能修改
container.items[0] = 10;   // ❌ 不能修改元素

四、多余属性检查(Excess Property Checks)

当使用对象字面量直接赋值给接口类型时,TypeScript 会检查是否有接口定义之外的属性。

typescript 复制代码
interface User {
    name: string;
    age: number;
}

// 错误:对象字面量中的 email 不是 User 的已知属性
const user: User = {
    name: "Alice",
    age: 25,
    email: "alice@example.com"  // ❌ 多余属性
};

但如果先创建对象再赋值,多余属性不会被检查:

typescript 复制代码
const obj = {
    name: "Alice",
    age: 25,
    email: "alice@example.com"
};
const user: User = obj;  // OK,没有多余属性检查

4.1 绕过多余属性检查的方法

方法一:使用类型断言

typescript 复制代码
const user = {
    name: "Alice",
    age: 25,
    email: "alice@example.com"
} as User;  // 断言,绕过检查

方法二:添加索引签名

如果接口知道将来可能有额外属性,可以定义索引签名。

typescript 复制代码
interface User {
    name: string;
    age: number;
    [key: string]: unknown;  // 允许任意额外属性
}

const user: User = {
    name: "Alice",
    age: 25,
    email: "alice@example.com"  // OK
};

方法三:先赋值给变量

如前所述,先将对象赋值给变量再传递给接口类型变量。

多余属性检查只在对象字面量直接赋值时生效,目的是捕获常见的拼写错误或属性名错误。

五、接口与类型别名的对比

interfacetype 都可以定义对象形状,但各有侧重。

特性 interface type
定义对象形状
定义联合类型
定义元组
定义基本类型别名
声明合并 ✅(同名接口自动合并) ❌(同名声名会报错)
继承/扩展 extends 关键字 & 交叉类型
实现类(implements ✅(但类实现接口更常见)
性能 更友好(缓存、错误信息) 某些复杂类型可能报错较长

5.1 声明合并

接口可以重复定义,TypeScript 会自动合并:

typescript 复制代码
interface Box {
    width: number;
}
interface Box {
    height: number;
}
// 等价于
interface Box {
    width: number;
    height: number;
}

类型别名不能重复定义。

5.2 扩展方式对比

typescript 复制代码
// 接口继承
interface Animal {
    name: string;
}
interface Dog extends Animal {
    bark(): void;
}

// 类型别名交叉
type Animal = { name: string };
type Dog = Animal & { bark(): void };

5.3 选择建议

  • 定义对象形状(尤其是会被扩展或实现的)优先用 interface,因为声明合并和更清晰的错误信息。
  • 定义联合类型、元组、基本类型别名、工具类型等用 type
  • 库的公共 API 推荐用 interface,方便用户通过声明合并扩展。

六、接口的函数类型和索引签名(简介)

接口不仅可以描述对象属性,还可以描述函数类型和索引签名。

6.1 函数类型接口

typescript 复制代码
interface StringTransformer {
    (input: string): string;
}

const toUpper: StringTransformer = (s) => s.toUpperCase();

6.2 索引签名

typescript 复制代码
interface StringArray {
    [index: number]: string;
}

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

索引签名可以是字符串或数字,用于描述动态属性。

typescript 复制代码
interface Dictionary {
    [key: string]: string;
}

const dict: Dictionary = {
    hello: "world",
    foo: "bar"
};

注意:数字索引的返回值类型必须是字符串索引返回值类型的子类型(因为 JavaScript 会将数字索引转换为字符串)。

七、常见错误与注意事项

7.1 忘记使用 = 定义接口

typescript 复制代码
interface User = { name: string };  // ❌ 接口不需要等号

7.2 使用 interface 定义联合类型

typescript 复制代码
interface Status = "on" | "off";  // ❌ 接口不能定义联合,用 type

7.3 只读属性与 const 混淆

readonly 表示属性本身不能重新赋值,但如果属性是对象,其内部属性仍可修改。

typescript 复制代码
interface Config {
    readonly settings: { level: number };
}

const cfg: Config = { settings: { level: 1 } };
cfg.settings.level = 2;  // OK,内部可变
cfg.settings = { level: 3 }; // ❌ 不能重新赋值 settings

如需深层只读,可使用 Readonly<T> 工具类型或 as const

7.4 多余属性检查意外触发

当函数参数直接传入对象字面量时会触发多余属性检查,提前设计可选属性或索引签名可避免。

typescript 复制代码
function processUser(user: User) {}

// 触发多余属性检查
processUser({ name: "Alice", age: 25, email: "a@b.com" }); // ❌

// 不触发
const extra = { name: "Alice", age: 25, email: "a@b.com" };
processUser(extra); // OK

八、综合示例

typescript 复制代码
// 定义用户接口
interface User {
    readonly id: number;
    name: string;
    email?: string;
    createdAt: Date;
}

// 定义管理员接口,继承 User
interface Admin extends User {
    role: "admin";
    permissions: string[];
}

// 普通用户
interface RegularUser extends User {
    role: "user";
    lastLogin?: Date;
}

type AppUser = Admin | RegularUser;

// 创建用户的工厂函数
function createUser(id: number, name: string, role: "admin" | "user"): AppUser {
    const base = {
        id,
        name,
        createdAt: new Date()
    };
    
    if (role === "admin") {
        return {
            ...base,
            role: "admin",
            permissions: ["read", "write"]
        };
    } else {
        return {
            ...base,
            role: "user"
        };
    }
}

// 使用
const admin = createUser(1, "Alice", "admin");
console.log(admin.permissions);  // ["read", "write"]
// admin.id = 2; // ❌ 只读属性

// 索引签名示例:配置对象
interface Settings {
    theme: "light" | "dark";
    fontSize: number;
    [key: string]: unknown;  // 允许其他扩展配置
}

const settings: Settings = {
    theme: "dark",
    fontSize: 14,
    experimentalFeature: true,  // OK
    customKey: "value"
};

// 接口声明合并
interface Car {
    brand: string;
}
interface Car {
    model: string;
}
const myCar: Car = { brand: "Toyota", model: "Camry" };

九、小结

概念 语法示例 说明
接口定义 interface User { name: string; } 描述对象形状
可选属性 age?: number 属性可以不存在
只读属性 readonly id: number 创建后不可修改
多余属性检查 对象字面量直接赋值时触发 可用断言或索引签名绕过
函数类型接口 { (x: number): string } 描述函数类型
索引签名 { [key: string]: unknown } 描述动态属性
接口合并 同名接口自动合并 利于扩展
vs type 对象形状优先用 interface 联合/元组用 type

觉得文章有帮助?别忘了:

👍 点赞 👍 -- 给我一点鼓励
⭐ 收藏 ⭐ -- 方便以后查看
🔔 关注 🔔 -- 获取更新通知


标签: #TypeScript #接口 #interface #对象形状 #学习笔记 #前端开发

相关推荐
铁皮饭盒2 小时前
成为AI全栈 - 第3课:路由 RESTful Elysia 状态码 设计规范
前端·后端·全栈
顾昂_2 小时前
Web 性能优化完全指南
前端·面试·性能优化
IT乐手2 小时前
Claude Code + Qwen 的配置方法
javascript·claude
前端程序媛-Tian3 小时前
前端 AI 提效实战:从 0 到 1 打造团队专属 AI 代码评审工具
前端·人工智能·ai
支付宝体验科技3 小时前
Ant Design Pro v6.0.0 发布
前端
T畅N3 小时前
审批流设计器(前端)
前端·elementui·vue·html·流程图·js
AlunYegeer3 小时前
JAVA,以后端的视角理解前端。在全栈的路上迈出第一步。
java·开发语言·前端
IT_陈寒4 小时前
Redis这个内存杀手,差点让我们运维半夜追杀我
前端·人工智能·后端
子兮曰4 小时前
DeepSeek TUI:原生 Rust 打造的终端 AI 编码 Agent
前端·javascript·后端