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
};

方法三:先赋值给变量

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

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

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

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

特性 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 #对象形状 #学习笔记 #前端开发

相关推荐
卸任19 小时前
Vibe coding工具使用心得
前端·ai编程
brien919 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance20 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M20 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写20 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李20 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易20 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding20 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马20 小时前
前沿视界
前端
kyriewen20 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员