3、接口(Interface):接口的定义、可选属性、只读属性、函数类型接口、索引签名、接口继承

接口这东西,说白了就是 TypeScript 里用来给对象「定规矩」的。我刚开始用 TS 时,觉得它跟 type 差不多,后来项目做大了才发现,接口在扩展性和可读性上确实有它的独到之处。咱们今天就把接口的几大核心特性捋一遍。

3.1 接口的定义

接口的定义很简单,用 interface 关键字就行。它描述了一个对象应该长什么样——有哪些属性,每个属性是什么类型。

interface User {
  name: string;
  age: number;
  email: string;
}

const user: User = {
  name: '张三',
  age: 28,
  email: 'zhangsan@example.com'
};

你看,User 接口规定了三个属性,少一个都不行。我在项目中经常用接口来定义后端返回的数据结构,这样前后端对接时,类型一目了然,少了很多扯皮。

核心要点:接口只做类型描述,不做任何实现。它就像一份合同,签了就得按规矩来。

3.2 可选属性

实际开发中,有些属性不是必须的。比如用户信息,可能有的用户没填手机号。这时候就用 ? 标记为可选。

interface User {
  name: string;
  age: number;
  phone?: string;  // 可选
  address?: string; // 可选
}

const user1: User = { name: '李四', age: 25 }; // 没问题
const user2: User = { name: '王五', age: 30, phone: '13800138000' }; // 也行

嗯,这里要注意:可选属性在访问时,TypeScript 会提醒你它可能为 undefined。所以用之前最好做一下判断,或者用可选链操作符 ?.

我的习惯:定义接口时,先把必填属性列出来,再考虑哪些可以选填。这样接口的语义更清晰,别人一看就知道哪些是核心数据。

3.3 只读属性

有些属性一旦初始化就不该被修改。比如用户的 ID、创建时间等。用 readonly 关键字就能搞定。

interface User {
  readonly id: number;
  readonly createdAt: Date;
  name: string;
  age: number;
}

const user: User = {
  id: 1001,
  createdAt: new Date(),
  name: '赵六',
  age: 22
};

// user.id = 1002; // 报错!只读属性不能修改

我曾经在项目中犯过这个错——用户 ID 被不小心改了,导致数据错乱。后来把所有 ID 字段都加上 readonly,这类问题再没出现过。避坑指南:凡是业务上不该变的值,一律用 readonly 锁死。

注意:readonly 只在编译阶段检查,运行时并不阻止修改。它更多是给开发者看的「请勿修改」提示。

3.4 函数类型接口

接口不仅能描述对象,还能描述函数。你想想看,有时候我们需要定义一组函数的签名,比如回调函数、事件处理函数等。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

const mySearch: SearchFunc = function(src, sub) {
  return src.includes(sub);
};

console.log(mySearch('Hello TypeScript', 'Type')); // true

我个人习惯用函数类型接口来定义事件处理器或工具函数的类型。这样在写高阶函数时,类型推导会非常顺畅,IDE 的智能提示也跟得上。

对比一下:type 也能定义函数类型:type SearchFunc = (source: string, subString: string) => boolean;。两者效果差不多,但接口更擅长扩展,后面会讲到。

3.5 索引签名

有时候我们不知道对象具体有哪些属性,但知道属性值的类型。比如一个配置对象,可以动态添加任意键值对。这时候就用索引签名。

interface StringArray {
  [index: number]: string;
}

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

// 更常见的场景:动态对象
interface Dict {
  [key: string]: string | number;
}

const config: Dict = {
  host: 'localhost',
  port: 8080,
  env: 'production'
};

我在项目中遇到过这样一个场景:后端返回的字段名不确定,但值类型是固定的。用索引签名一把搞定,不用一个个去定义。不过要注意,索引签名一旦定义,所有属性都必须符合它的类型约束。

小技巧:索引签名可以和具体属性混用,但具体属性的类型必须是索引签名类型的子集。比如 [key: string]: string 时,就不能有 age: number 这样的属性。

3.6 接口继承

接口可以继承另一个接口,把公共字段提取出来复用。这跟类的继承有点像,但接口继承更灵活,可以同时继承多个。

interface BaseUser {
  id: number;
  name: string;
}

interface Admin extends BaseUser {
  role: 'admin';
  permissions: string[];
}

interface VipUser extends BaseUser {
  level: number;
  expireDate: Date;
}

const admin: Admin = {
  id: 1,
  name: '管理员',
  role: 'admin',
  permissions: ['read', 'write', 'delete']
};

你想想看,如果没有继承,每个接口都得重复写 idname,维护起来多累。接口继承还能多层嵌套,比如 SuperAdmin extends Admin,这样代码复用率就很高了。

注意:接口继承时,如果父接口有同名属性,子接口的类型必须兼容父接口的类型。说白了就是不能「越改越小」。

3.7 接口 vs 类型别名

很多初学者会纠结用 interface 还是 type。我个人的经验是:

特性 interface type
对象类型 ✅ 推荐 ✅ 可用
函数类型 ✅ 可用 ✅ 推荐(更简洁)
联合类型 ❌ 不支持 ✅ 支持
继承/扩展 ✅ 支持(extends) ✅ 支持(交叉类型 &)
声明合并 ✅ 支持 ❌ 不支持

说白了,定义对象结构时优先用 interface,需要联合类型或元组时用 type。两者不是非此即彼的关系,可以混用。

避坑指南:我曾经在项目中用 type 定义了一个对象类型,后来需要扩展时发现不能像接口那样直接 extends,只能改用交叉类型。虽然也能实现,但可读性差了不少。所以,如果你确定要定义的是「对象形状」,直接用 interface 更省心。

好了,接口这块的内容就这些。从定义到可选属性、只读属性,再到函数类型、索引签名和继承,每个特性都有它的用武之地。下一章咱们聊聊类与继承,看看 TypeScript 的类跟 ES6 的类有什么不一样。