接口(interface)详解
本教程共 80 篇 · 第 24 篇 · 更新于 2026-08-10 · 约 10 分钟阅读
本节目标:掌握 interface 的完整语法,能用它定义对象的形状、函数的签名和动态键值对的类型。
上一章我们在函数参数里直接写了 { x: number; y: number } 这样的类型注解。这种写法用一两次还行,但如果同一个形状到处出现,每次都要重写一遍,又丑又容易出错。
这时候就该请出 TypeScript 专门为对象设计的一等公民——接口(interface)。
接口的基本语法
接口用 interface 关键字定义,给对象形状起个名字:
interface Point {
x: number;
y: number;
}
let p1: Point = { x: 10, y: 20 };
let p2: Point = { x: 5, y: 0 };
给形状起了名字之后,到处都可以用 Point 而不用重复写类型。而且接口本质上就是一份”契约”——任何被标注为 Point 类型的对象,都必须提供 x 和 y 这两个数字属性。
Note接口只存在于编译时。编译成 JavaScript 后,接口会被完全抹掉,零运行时开销。
可选属性
跟对象字面量一样,接口里也可以用 ? 标记可选属性:
interface User {
name: string;
age?: number;
email?: string;
}
const u1: User = { name: "Alice" }; // ✅
const u2: User = { name: "Bob", age: 30 }; // ✅
const u3: User = { name: "Carol", age: 25, email: "c@x.com" }; // ✅
在 strict 模式下(TypeScript 7.0 默认),age? 的类型是 number | undefined。这意味着你读取 u1.age 时,TypeScript 会强制你处理 undefined 的情况:
// ❌ 不能直接用,可能是 undefined
// console.log(u1.age.toFixed(2));
// ✅ 先检查
if (u1.age !== undefined) {
console.log(u1.age.toFixed(2));
}
// ✅ 或者用可选链(后面章节会详细讲)
console.log(u1.age?.toFixed(2));
只读属性
有些属性创建之后就不该被改了。用 readonly 标记:
interface Config {
readonly apiUrl: string;
readonly timeout: number;
retryCount: number;
}
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retryCount: 3,
};
config.retryCount = 5; // ✅ 普通属性可以改
// config.apiUrl = "..."; // ❌ 只读属性不能改
Tip
readonly是编译时约束,运行时没有任何保护。如果你的代码被编译成 JS 后在不检查类型的环境跑,只读属性仍然可以被修改。需要真正的不可变数据请用Object.freeze()。
不过有个坑:readonly 只作用于第一层。如果属性本身是个对象,对象内部的属性仍然可以改——这个问题我们在第 26 章细聊。
函数类型接口
接口不仅能描述”有属性的对象”,还能描述”函数本身”:
interface GreetFn {
(name: string): string;
}
const greet: GreetFn = (name) => `你好,${name}!`;
console.log(greet("小明")); // "你好,小明!"
这种写法叫调用签名(call signature)。它跟 type GreetFn = (name: string) => string 效果一样,但接口版本可以额外挂属性:
interface GreetFn {
(name: string): string;
description: string;
defaultName: string;
}
// 函数也是对象,可以挂属性
const greet = ((name: string) => `你好,${name}!`) as GreetFn;
greet.description = "一个简单的问候函数";
greet.defaultName = "世界";
这种”函数带属性”的模式在第三方库里挺常见(比如 jQuery 的 $ 既是函数又有 $.ajax 等方法)。
索引签名
有些对象的键名是动态的——比如用户字典、配置映射表。你没法提前列出所有键,但你知道值的类型。这时候用索引签名:
interface StringDict {
[key: string]: string;
}
const dict: StringDict = {
hello: "你好",
world: "世界",
foo: "哎哟",
};
dict["bar"] = "巴"; // ✅ 动态添加也没问题
// dict["count"] = 42; // ❌ 值必须是 string
索引签名的语法是 [key: string]: 值类型。key 只是个占位名,写 [k: string] 或 [prop: string] 都行。
数字索引
索引键也可以是 number:
interface StringArray {
[index: number]: string;
}
const arr: StringArray = ["零", "壹", "贰"];
console.log(arr[1]); // "壹"
不过要注意:JavaScript 里所有对象的键最终都是字符串,arr[0] 实际上访问的是 arr["0"]。TypeScript 区分数字索引主要是为了配合数组的使用习惯。第 26 章会深入讲数字索引和字符串索引之间的约束关系。
索引签名 + 已知属性混用
你可以把索引签名和明确的属性写在一起:
interface UserRecord {
id: number; // 固定属性
name: string; // 固定属性
[key: string]: string | number; // 其他动态属性
}
const rec: UserRecord = {
id: 1,
name: "Alice",
role: "admin", // ✅ 动态属性,值是 string
score: 95, // ✅ 动态属性,值是 number
// isAdmin: true, // ❌ boolean 不在 string | number 里
};
一个重要的约束:所有固定属性的类型,必须能赋值给索引签名的值类型。 比如上面 id: number 和 name: string 都能赋值给 string | number,所以没问题。如果索引签名写的是 [key: string]: string,那 id: number 就会报错,因为 number 不能赋值给 string。
接口描述对象形状
总结一下,一个接口就是一份”对象使用说明书”。它告诉所有使用这个类型的代码:
- 这个对象有哪些属性
- 每个属性是什么类型
- 哪些属性必须有、哪些可以没有
- 哪些属性创建后不能改
- 动态的键对应什么类型的值
以现实中的例子类比的话,接口就像一份租房合同——合同上写了租期、租金、押金这些条款,签了就得照办。TypeScript 的接口对于对象来说,就是这样的合同。
interface Product {
readonly id: number;
name: string;
price: number;
description?: string;
tags: string[];
[customField: string]: unknown;
}
const item: Product = {
id: 1,
name: "无线耳机",
price: 299,
tags: ["电子", "音频"],
color: "黑色", // 索引签名兜底
weight: "250g", // 索引签名兜底
};
这个 Product 接口清晰地定义了:id 只读、name 和 price 必填、description 可选、tags 是字符串数组,其余字段可以是任意类型。
小结
interface给对象形状起名,便于复用,编译后零开销。- 可选属性
?在 strict 模式下类型包含undefined。 readonly防止属性被重新赋值,但只约束第一层。- 调用签名
(参数): 返回值让接口能描述函数类型。 - 索引签名
[key: string]: T处理动态键名的对象。 - 固定属性必须兼容索引签名的值类型。
接口已经很强了,但 TypeScript 还有更厉害的:接口可以继承其他接口。下一章见。